├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── rustfmt.toml └── src ├── diffur.rs └── tui.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | on: [push] 3 | 4 | jobs: 5 | cargo-tests: 6 | runs-on: ubuntu-latest 7 | timeout-minutes: 20 8 | steps: 9 | - name: Checkout sources 10 | uses: actions/checkout@v3 11 | - name: Install Rust toolchain 12 | uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: stable 15 | profile: minimal 16 | override: true 17 | - uses: Swatinem/rust-cache@v1 18 | with: 19 | cache-on-failure: true 20 | - name: cargo test 21 | run: cargo test --all --all-features 22 | cargo-lint: 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 20 25 | steps: 26 | - name: Checkout sources 27 | uses: actions/checkout@v3 28 | - name: Install Rust toolchain 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: nightly 32 | profile: minimal 33 | components: rustfmt, clippy 34 | override: true 35 | - uses: Swatinem/rust-cache@v1 36 | with: 37 | cache-on-failure: true 38 | - name: cargo fmt 39 | run: cargo +nightly fmt --all -- --check 40 | - name: cargo clippy 41 | run: cargo +nightly clippy --all --all-features -- -D warnings 42 | cargo-build: 43 | runs-on: ubuntu-latest 44 | timeout-minutes: 20 45 | continue-on-error: true 46 | steps: 47 | - name: Checkout sources 48 | uses: actions/checkout@v3 49 | - name: Install Rust toolchain 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | toolchain: stable 53 | profile: minimal 54 | override: true 55 | - uses: Swatinem/rust-cache@v1 56 | with: 57 | cache-on-failure: true 58 | - name: build 59 | id: build 60 | continue-on-error: true 61 | run: cargo build --all 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | .DS_Store 3 | 4 | # Rust 5 | /target 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "allocator-api2" 19 | version = "0.2.18" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 22 | 23 | [[package]] 24 | name = "anyhow" 25 | version = "1.0.82" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" 28 | 29 | [[package]] 30 | name = "autocfg" 31 | version = "1.3.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 34 | 35 | [[package]] 36 | name = "bitflags" 37 | version = "2.5.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 40 | 41 | [[package]] 42 | name = "cassowary" 43 | version = "0.3.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 46 | 47 | [[package]] 48 | name = "castaway" 49 | version = "0.2.2" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 52 | dependencies = [ 53 | "rustversion", 54 | ] 55 | 56 | [[package]] 57 | name = "cfg-if" 58 | version = "1.0.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 61 | 62 | [[package]] 63 | name = "compact_str" 64 | version = "0.7.1" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 67 | dependencies = [ 68 | "castaway", 69 | "cfg-if", 70 | "itoa", 71 | "ryu", 72 | "static_assertions", 73 | ] 74 | 75 | [[package]] 76 | name = "crossterm" 77 | version = "0.27.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 80 | dependencies = [ 81 | "bitflags", 82 | "crossterm_winapi", 83 | "libc", 84 | "mio", 85 | "parking_lot", 86 | "signal-hook", 87 | "signal-hook-mio", 88 | "winapi", 89 | ] 90 | 91 | [[package]] 92 | name = "crossterm_winapi" 93 | version = "0.9.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 96 | dependencies = [ 97 | "winapi", 98 | ] 99 | 100 | [[package]] 101 | name = "diffur" 102 | version = "0.1.0" 103 | dependencies = [ 104 | "anyhow", 105 | "crossterm", 106 | "itertools", 107 | "ratatui", 108 | "tempfile", 109 | ] 110 | 111 | [[package]] 112 | name = "either" 113 | version = "1.11.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 116 | 117 | [[package]] 118 | name = "errno" 119 | version = "0.3.9" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 122 | dependencies = [ 123 | "libc", 124 | "windows-sys 0.52.0", 125 | ] 126 | 127 | [[package]] 128 | name = "fastrand" 129 | version = "2.1.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 132 | 133 | [[package]] 134 | name = "hashbrown" 135 | version = "0.14.5" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 138 | dependencies = [ 139 | "ahash", 140 | "allocator-api2", 141 | ] 142 | 143 | [[package]] 144 | name = "heck" 145 | version = "0.4.1" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 148 | 149 | [[package]] 150 | name = "indoc" 151 | version = "2.0.5" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 154 | 155 | [[package]] 156 | name = "itertools" 157 | version = "0.12.1" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 160 | dependencies = [ 161 | "either", 162 | ] 163 | 164 | [[package]] 165 | name = "itoa" 166 | version = "1.0.11" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 169 | 170 | [[package]] 171 | name = "libc" 172 | version = "0.2.154" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 175 | 176 | [[package]] 177 | name = "linux-raw-sys" 178 | version = "0.4.13" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 181 | 182 | [[package]] 183 | name = "lock_api" 184 | version = "0.4.12" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 187 | dependencies = [ 188 | "autocfg", 189 | "scopeguard", 190 | ] 191 | 192 | [[package]] 193 | name = "log" 194 | version = "0.4.21" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 197 | 198 | [[package]] 199 | name = "lru" 200 | version = "0.12.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 203 | dependencies = [ 204 | "hashbrown", 205 | ] 206 | 207 | [[package]] 208 | name = "mio" 209 | version = "0.8.11" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 212 | dependencies = [ 213 | "libc", 214 | "log", 215 | "wasi", 216 | "windows-sys 0.48.0", 217 | ] 218 | 219 | [[package]] 220 | name = "once_cell" 221 | version = "1.19.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 224 | 225 | [[package]] 226 | name = "parking_lot" 227 | version = "0.12.2" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" 230 | dependencies = [ 231 | "lock_api", 232 | "parking_lot_core", 233 | ] 234 | 235 | [[package]] 236 | name = "parking_lot_core" 237 | version = "0.9.10" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 240 | dependencies = [ 241 | "cfg-if", 242 | "libc", 243 | "redox_syscall", 244 | "smallvec", 245 | "windows-targets 0.52.5", 246 | ] 247 | 248 | [[package]] 249 | name = "paste" 250 | version = "1.0.15" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 253 | 254 | [[package]] 255 | name = "proc-macro2" 256 | version = "1.0.81" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 259 | dependencies = [ 260 | "unicode-ident", 261 | ] 262 | 263 | [[package]] 264 | name = "quote" 265 | version = "1.0.36" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 268 | dependencies = [ 269 | "proc-macro2", 270 | ] 271 | 272 | [[package]] 273 | name = "ratatui" 274 | version = "0.26.2" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" 277 | dependencies = [ 278 | "bitflags", 279 | "cassowary", 280 | "compact_str", 281 | "crossterm", 282 | "indoc", 283 | "itertools", 284 | "lru", 285 | "paste", 286 | "stability", 287 | "strum", 288 | "unicode-segmentation", 289 | "unicode-width", 290 | ] 291 | 292 | [[package]] 293 | name = "redox_syscall" 294 | version = "0.5.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 297 | dependencies = [ 298 | "bitflags", 299 | ] 300 | 301 | [[package]] 302 | name = "rustix" 303 | version = "0.38.34" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 306 | dependencies = [ 307 | "bitflags", 308 | "errno", 309 | "libc", 310 | "linux-raw-sys", 311 | "windows-sys 0.52.0", 312 | ] 313 | 314 | [[package]] 315 | name = "rustversion" 316 | version = "1.0.16" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" 319 | 320 | [[package]] 321 | name = "ryu" 322 | version = "1.0.18" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 325 | 326 | [[package]] 327 | name = "scopeguard" 328 | version = "1.2.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 331 | 332 | [[package]] 333 | name = "signal-hook" 334 | version = "0.3.17" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 337 | dependencies = [ 338 | "libc", 339 | "signal-hook-registry", 340 | ] 341 | 342 | [[package]] 343 | name = "signal-hook-mio" 344 | version = "0.2.3" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 347 | dependencies = [ 348 | "libc", 349 | "mio", 350 | "signal-hook", 351 | ] 352 | 353 | [[package]] 354 | name = "signal-hook-registry" 355 | version = "1.4.2" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 358 | dependencies = [ 359 | "libc", 360 | ] 361 | 362 | [[package]] 363 | name = "smallvec" 364 | version = "1.13.2" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 367 | 368 | [[package]] 369 | name = "stability" 370 | version = "0.2.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" 373 | dependencies = [ 374 | "quote", 375 | "syn", 376 | ] 377 | 378 | [[package]] 379 | name = "static_assertions" 380 | version = "1.1.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 383 | 384 | [[package]] 385 | name = "strum" 386 | version = "0.26.2" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 389 | dependencies = [ 390 | "strum_macros", 391 | ] 392 | 393 | [[package]] 394 | name = "strum_macros" 395 | version = "0.26.2" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" 398 | dependencies = [ 399 | "heck", 400 | "proc-macro2", 401 | "quote", 402 | "rustversion", 403 | "syn", 404 | ] 405 | 406 | [[package]] 407 | name = "syn" 408 | version = "2.0.60" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 411 | dependencies = [ 412 | "proc-macro2", 413 | "quote", 414 | "unicode-ident", 415 | ] 416 | 417 | [[package]] 418 | name = "tempfile" 419 | version = "3.10.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 422 | dependencies = [ 423 | "cfg-if", 424 | "fastrand", 425 | "rustix", 426 | "windows-sys 0.52.0", 427 | ] 428 | 429 | [[package]] 430 | name = "unicode-ident" 431 | version = "1.0.12" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 434 | 435 | [[package]] 436 | name = "unicode-segmentation" 437 | version = "1.11.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 440 | 441 | [[package]] 442 | name = "unicode-width" 443 | version = "0.1.12" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 446 | 447 | [[package]] 448 | name = "version_check" 449 | version = "0.9.4" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 452 | 453 | [[package]] 454 | name = "wasi" 455 | version = "0.11.0+wasi-snapshot-preview1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 458 | 459 | [[package]] 460 | name = "winapi" 461 | version = "0.3.9" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 464 | dependencies = [ 465 | "winapi-i686-pc-windows-gnu", 466 | "winapi-x86_64-pc-windows-gnu", 467 | ] 468 | 469 | [[package]] 470 | name = "winapi-i686-pc-windows-gnu" 471 | version = "0.4.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 474 | 475 | [[package]] 476 | name = "winapi-x86_64-pc-windows-gnu" 477 | version = "0.4.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 480 | 481 | [[package]] 482 | name = "windows-sys" 483 | version = "0.48.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 486 | dependencies = [ 487 | "windows-targets 0.48.5", 488 | ] 489 | 490 | [[package]] 491 | name = "windows-sys" 492 | version = "0.52.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 495 | dependencies = [ 496 | "windows-targets 0.52.5", 497 | ] 498 | 499 | [[package]] 500 | name = "windows-targets" 501 | version = "0.48.5" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 504 | dependencies = [ 505 | "windows_aarch64_gnullvm 0.48.5", 506 | "windows_aarch64_msvc 0.48.5", 507 | "windows_i686_gnu 0.48.5", 508 | "windows_i686_msvc 0.48.5", 509 | "windows_x86_64_gnu 0.48.5", 510 | "windows_x86_64_gnullvm 0.48.5", 511 | "windows_x86_64_msvc 0.48.5", 512 | ] 513 | 514 | [[package]] 515 | name = "windows-targets" 516 | version = "0.52.5" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 519 | dependencies = [ 520 | "windows_aarch64_gnullvm 0.52.5", 521 | "windows_aarch64_msvc 0.52.5", 522 | "windows_i686_gnu 0.52.5", 523 | "windows_i686_gnullvm", 524 | "windows_i686_msvc 0.52.5", 525 | "windows_x86_64_gnu 0.52.5", 526 | "windows_x86_64_gnullvm 0.52.5", 527 | "windows_x86_64_msvc 0.52.5", 528 | ] 529 | 530 | [[package]] 531 | name = "windows_aarch64_gnullvm" 532 | version = "0.48.5" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 535 | 536 | [[package]] 537 | name = "windows_aarch64_gnullvm" 538 | version = "0.52.5" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 541 | 542 | [[package]] 543 | name = "windows_aarch64_msvc" 544 | version = "0.48.5" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 547 | 548 | [[package]] 549 | name = "windows_aarch64_msvc" 550 | version = "0.52.5" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 553 | 554 | [[package]] 555 | name = "windows_i686_gnu" 556 | version = "0.48.5" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 559 | 560 | [[package]] 561 | name = "windows_i686_gnu" 562 | version = "0.52.5" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 565 | 566 | [[package]] 567 | name = "windows_i686_gnullvm" 568 | version = "0.52.5" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 571 | 572 | [[package]] 573 | name = "windows_i686_msvc" 574 | version = "0.48.5" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 577 | 578 | [[package]] 579 | name = "windows_i686_msvc" 580 | version = "0.52.5" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 583 | 584 | [[package]] 585 | name = "windows_x86_64_gnu" 586 | version = "0.48.5" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 589 | 590 | [[package]] 591 | name = "windows_x86_64_gnu" 592 | version = "0.52.5" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 595 | 596 | [[package]] 597 | name = "windows_x86_64_gnullvm" 598 | version = "0.48.5" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 601 | 602 | [[package]] 603 | name = "windows_x86_64_gnullvm" 604 | version = "0.52.5" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 607 | 608 | [[package]] 609 | name = "windows_x86_64_msvc" 610 | version = "0.48.5" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 613 | 614 | [[package]] 615 | name = "windows_x86_64_msvc" 616 | version = "0.52.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 619 | 620 | [[package]] 621 | name = "zerocopy" 622 | version = "0.7.34" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 625 | dependencies = [ 626 | "zerocopy-derive", 627 | ] 628 | 629 | [[package]] 630 | name = "zerocopy-derive" 631 | version = "0.7.34" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 634 | dependencies = [ 635 | "proc-macro2", 636 | "quote", 637 | "syn", 638 | ] 639 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diffur" 3 | description = "A TUI for diffing text" 4 | authors = ["clabby"] 5 | version = "0.1.0" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | # External 10 | anyhow = "1.0.70" 11 | ratatui = "0.26.2" 12 | crossterm = "0.27.0" 13 | tempfile = "3.10.1" 14 | itertools = "0.12.1" 15 | 16 | [[bin]] 17 | name = "diffur" 18 | path = "src/diffur.rs" 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2023` `clabby` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `diffur` 2 | 3 | A small TUI that assists in diffing two bodies of text easily. Rather than creating two files and passing them to `diff`, 4 | this tool handles the tempfile creation and cleanup, as well quick launch hotkeys for editing the files and executing `diff`. 5 | 6 | https://github.com/clabby/diffur/assets/8406232/b8211eca-d77e-4758-822f-c80d8101447f 7 | 8 | ## `delta` 9 | 10 | This tool is meant to be used alongside [`delta`][delta], a custom pager for `diff` and `git`. To install `delta`, run: 11 | 12 | ```sh 13 | cargo install git-delta 14 | ``` 15 | 16 | ## Installation 17 | 18 | After `delta` has been installed, `diffur` can be installed by running: 19 | 20 | ```sh 21 | git clone git@github.com:clabby/diffur.git && cd diffur && cargo install --path . 22 | ``` 23 | 24 | [delta]: https://github.com/dandavison/delta 25 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | imports_granularity = "Crate" 3 | use_small_heuristics = "Max" 4 | comment_width = 100 5 | wrap_comments = true 6 | binop_separator = "Back" 7 | trailing_comma = "Vertical" 8 | trailing_semicolon = false 9 | use_field_init_shorthand = true 10 | format_code_in_doc_comments = true 11 | doc_comment_code_block_width = 100 12 | -------------------------------------------------------------------------------- /src/diffur.rs: -------------------------------------------------------------------------------- 1 | //! Diffur 2 | 3 | use anyhow::Result; 4 | use crossterm::{ 5 | event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, 6 | execute, 7 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 8 | ExecutableCommand, 9 | }; 10 | use ratatui::{ 11 | prelude::*, 12 | widgets::{Block, Paragraph, Wrap}, 13 | }; 14 | use std::{ 15 | io::{self}, 16 | process::Command, 17 | }; 18 | use tempfile::NamedTempFile; 19 | 20 | /// The TUI state 21 | struct App { 22 | /// Left temp file 23 | left: NamedTempFile, 24 | /// Right temp file 25 | right: NamedTempFile, 26 | } 27 | 28 | /// UpdateKind is an enum to represent the kind of update being performed to the app. 29 | enum UpdateKind { 30 | Left, 31 | Right, 32 | } 33 | 34 | impl App { 35 | fn new() -> Self { 36 | Self { 37 | left: NamedTempFile::new().expect("Failed to create temp file"), 38 | right: NamedTempFile::new().expect("Failed to create temp file"), 39 | } 40 | } 41 | } 42 | 43 | fn main() -> Result<()> { 44 | // setup terminal 45 | enable_raw_mode()?; 46 | let mut stdout = io::stdout(); 47 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 48 | let backend = CrosstermBackend::new(stdout); 49 | let mut terminal = Terminal::new(backend)?; 50 | 51 | // create app and run it 52 | let app = App::new(); 53 | let res = run_app(&mut terminal, app); 54 | 55 | // restore terminal 56 | disable_raw_mode()?; 57 | execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; 58 | terminal.show_cursor()?; 59 | 60 | if let Err(err) = res { 61 | println!("{err:?}"); 62 | } 63 | 64 | Ok(()) 65 | } 66 | 67 | /// Starts the blocking event loop 68 | fn run_app(terminal: &mut Terminal, app: App) -> io::Result<()> { 69 | loop { 70 | terminal.draw(|f| ui(f, &app))?; 71 | 72 | if let Event::Key(key) = event::read()? { 73 | match key.code { 74 | KeyCode::Char('a') => { 75 | launch_editor(&app, UpdateKind::Left, terminal) 76 | .map_err(|_| io::ErrorKind::BrokenPipe)?; 77 | } 78 | KeyCode::Char('b') => { 79 | launch_editor(&app, UpdateKind::Right, terminal) 80 | .map_err(|_| io::ErrorKind::BrokenPipe)?; 81 | } 82 | KeyCode::Char('c') => { 83 | app.left.as_file().set_len(0)?; 84 | app.right.as_file().set_len(0)?; 85 | } 86 | KeyCode::Char('d') => { 87 | diff_files(&app, terminal).map_err(|_| io::ErrorKind::BrokenPipe)?; 88 | } 89 | KeyCode::Char('q') => { 90 | return Ok(()); 91 | } 92 | _ => {} 93 | } 94 | } 95 | } 96 | } 97 | 98 | /// Renders the UI 99 | fn ui(f: &mut Frame, app: &App) { 100 | let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]); 101 | let [help_area, input_area] = vertical.areas(f.size()); 102 | let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); 103 | let [input_left_area, input_right_area] = horizontal.areas(input_area); 104 | 105 | // Render the help message 106 | let help_message = help_widget(); 107 | f.render_widget(help_message, help_area); 108 | 109 | // Get file contents 110 | let left_contents = std::fs::read_to_string(app.left.path()).unwrap_or_default(); 111 | let right_contents = std::fs::read_to_string(app.right.path()).unwrap_or_default(); 112 | 113 | // Render the left input box 114 | let input_left = Paragraph::new(left_contents) 115 | .style(Style::default()) 116 | .wrap(Wrap { trim: false }) 117 | .block(Block::bordered().title("Contents (Left)")); 118 | f.render_widget(input_left, input_left_area); 119 | 120 | // Render the right input box 121 | let input_right = Paragraph::new(right_contents) 122 | .style(Style::default()) 123 | .wrap(Wrap { trim: false }) 124 | .block(Block::bordered().title("Contents (Right)")); 125 | f.render_widget(input_right, input_right_area); 126 | } 127 | 128 | fn help_widget() -> Paragraph<'static> { 129 | let (msg, style) = ( 130 | vec![ 131 | "[q]".green().bold(), 132 | " Quit - ".into(), 133 | "[a]".green().bold(), 134 | " edit left - ".into(), 135 | "[b]".green().bold(), 136 | " edit right - ".into(), 137 | "[c]".green().bold(), 138 | " clear input - ".into(), 139 | "[d]".green().bold(), 140 | " diff text".into(), 141 | ], 142 | Style::default().add_modifier(Modifier::RAPID_BLINK), 143 | ); 144 | let text = Text::from(Line::from(msg)).patch_style(style); 145 | Paragraph::new(text) 146 | } 147 | 148 | /// Launches the editor to edit a file in the [App]. 149 | fn launch_editor( 150 | app: &App, 151 | update_kind: UpdateKind, 152 | terminal: &mut Terminal, 153 | ) -> Result<()> { 154 | let mut stdout = io::stdout(); 155 | 156 | // Exit the alternate screen to return to the normal terminal screen 157 | stdout.execute(LeaveAlternateScreen)?; 158 | 159 | // Disable raw mode to hand over the terminal to Vim properly 160 | disable_raw_mode()?; 161 | 162 | // Get the preferred editor from the environment 163 | let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string()); 164 | 165 | // Use the standard library to call Vim 166 | let path = match update_kind { 167 | UpdateKind::Left => app.left.path(), 168 | UpdateKind::Right => app.right.path(), 169 | }; 170 | Command::new(editor).arg(path).status().expect("failed to edit"); 171 | 172 | // Clear the screen and re-enable raw mode when Vim exits 173 | enable_raw_mode()?; 174 | 175 | // Enter the alternate screen again 176 | stdout.execute(EnterAlternateScreen)?; 177 | 178 | terminal.clear()?; 179 | terminal.draw(|f| ui(f, app))?; 180 | Ok(()) 181 | } 182 | 183 | /// Launches `delta` to diff the two files in the [App]. 184 | fn diff_files(app: &App, terminal: &mut Terminal) -> Result<()> { 185 | let mut stdout = io::stdout(); 186 | 187 | // Exit the alternate screen to return to the normal terminal screen 188 | stdout.execute(LeaveAlternateScreen)?; 189 | 190 | // Disable raw mode to hand over the terminal to Vim properly 191 | disable_raw_mode()?; 192 | 193 | // Use the standard library to call Vim 194 | Command::new("delta") 195 | .arg(app.left.path()) 196 | .arg(app.right.path()) 197 | .arg("--paging") 198 | .arg("always") 199 | .status() 200 | .expect("failed to diff"); 201 | 202 | // Clear the screen and re-enable raw mode when Vim exits 203 | enable_raw_mode()?; 204 | 205 | // Enter the alternate screen again 206 | stdout.execute(EnterAlternateScreen)?; 207 | 208 | terminal.clear()?; 209 | terminal.draw(|f| ui(f, app))?; 210 | Ok(()) 211 | } 212 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clabby/diffur/895c9e2a7817da8e152b7b85a9dc1cd41bf66109/src/tui.rs --------------------------------------------------------------------------------