Compare commits

...

22 Commits

Author SHA1 Message Date
83de98b5ee Version v2.3.1 2024-10-10 22:58:33 -06:00
c1ba4e91d1 ci: Setup python venv for cargo-zigbuild 2024-10-10 22:39:31 -06:00
575900024d Avoid resetting diff state on unit config reload 2024-10-10 22:31:04 -06:00
cbe299e859 Fix logic issue with 0-sized symbols
Fixes #119
2024-10-10 22:20:48 -06:00
741d93e211 Add symbol mapping feature (#118)
This allows users to "map" (or "link") symbols with different names so that they can be compared without having to update either the target or base objects. Symbol mappings are persisted in objdiff.json, so generators will need to ensure that they're preserved when updating. (Example: d1334bb79e)

Resolves #117
2024-10-09 21:44:18 -06:00
603dbd6882 Round match percent down before display
Ensures that 100% isn't displayed until it's a
perfect match.
2024-10-07 20:17:56 -06:00
6fb0a63de2 Click on empty space in row to clear highlight
Resolves #116
2024-10-07 19:53:16 -06:00
ab2e84a2c6 Deprioritize generated GCC symbols in find_section_symbol
Resolves #115
2024-10-07 19:49:52 -06:00
9596051cb4 Allow collapsing sidebar in symbols view 2024-10-07 19:46:16 -06:00
a5d9d8282e Update all dependencies 2024-10-03 22:00:43 -06:00
Amber Brault
3287a0f65c Bump cwextab again to 1.0.2 (#114)
* Bump cwextab

* Updated cwextab to not error on null actions

* Bump cwextab again
2024-10-03 01:12:37 -06:00
Amber Brault
fab9c62dfb Bump cwextab (#113)
* Bump cwextab

* Updated cwextab to not error on null actions
2024-10-01 23:20:09 -06:00
08cd768260 Add total_units, complete_units to progress report 2024-09-30 21:41:57 -06:00
8acaaf528c Version v2.2.0 2024-09-29 12:26:41 -06:00
6e881a74e1 Remove armv7-unknown-linux-musleabi build 2024-09-29 11:57:13 -06:00
cc1bc44e69 Use mimalloc when targeting musl 2024-09-29 11:52:04 -06:00
c7b85518ab Rework jobs view & error handling improvements
Job status is now shown in the top menu bar,
with a new Jobs window that can be toggled.

Build and diff errors are now handled more
gracefully.

Fixes #40
2024-09-28 12:14:20 -06:00
bb039a1445 Add "Open source file" option
Available when right-clicking an object in
the object list or when viewing an object

Resolves #99
2024-09-28 11:50:56 -06:00
8fc142d316 Debounce loaded object modification check
Before, this was running 2 fs::metadata
calls every frame. We don't need to do it
nearly that often, so now it only checks
once every 500ms.

This required refactoring AppConfig into
a separate AppState that holds transient
runtime state along with the loaded
AppConfig.
2024-09-28 10:55:22 -06:00
b0123b3f83 Improve build log message when command doesn't exist
Before, it didn't include the actual command
that was attempted to run.
2024-09-28 10:55:09 -06:00
2ec17aee9b Improve config read/write performance
We were accidentally using unbuffered readers
and writers before, leading to long pauses on
the main thread on slow filesystems. (e.g.
FUSE or WSL)
2024-09-28 10:54:54 -06:00
ec9731e1e5 Set app_id in eframe NativeOptions
Fixes missing WM_CLASS on Wayland
2024-09-28 10:53:58 -06:00
36 changed files with 3239 additions and 1440 deletions

View File

@@ -110,11 +110,6 @@ jobs:
name: linux-aarch64 name: linux-aarch64
build: zigbuild build: zigbuild
features: default features: default
- platform: ubuntu-latest
target: armv7-unknown-linux-musleabi
name: linux-armv7l
build: zigbuild
features: default
- platform: windows-latest - platform: windows-latest
target: i686-pc-windows-msvc target: i686-pc-windows-msvc
name: windows-x86 name: windows-x86
@@ -147,7 +142,11 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
if: matrix.build == 'zigbuild' if: matrix.build == 'zigbuild'
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1 run: |
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:

294
Cargo.lock generated
View File

@@ -37,12 +37,6 @@ dependencies = [
"gimli", "gimli",
] ]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.0" version = "2.0.0"
@@ -324,7 +318,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -359,7 +353,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -383,7 +377,7 @@ dependencies = [
"addr2line", "addr2line",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide 0.8.0", "miniz_oxide",
"object", "object",
"rustc-demangle", "rustc-demangle",
"windows-targets 0.52.6", "windows-targets 0.52.6",
@@ -401,6 +395,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bimap"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.6.0" version = "0.6.0"
@@ -501,7 +504,7 @@ checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -565,9 +568,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.21" version = "1.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@@ -906,9 +909,9 @@ checksum = "c2e06f9bce634a3c898eb1e5cb949ff63133cbb218af93cc9b38b31d6f3ea285"
[[package]] [[package]]
name = "cwextab" name = "cwextab"
version = "0.3.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f1036150ed9aa3265b83b9755a14db1600231e0478e678241e4f4d7c30bcf6" checksum = "e5aa7f13cc2fcb2bcfd3abc51bdbbf8f1fb729a69ed8c05ecbaa1a42197d1842"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
@@ -1014,9 +1017,9 @@ dependencies = [
[[package]] [[package]]
name = "ecolor" name = "ecolor"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5629649a8ae57c73f175f4a96419905a8102cfbfcbce96ea25a826bbf468e990" checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"emath", "emath",
@@ -1025,9 +1028,9 @@ dependencies = [
[[package]] [[package]]
name = "eframe" name = "eframe"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712634e63d86f5eb8e30f880bc4803b79dcc82539aec1a28fde86ed839daed24" checksum = "8ac2645a9bf4826eb4e91488b1f17b8eaddeef09396706b2f14066461338e24f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytemuck", "bytemuck",
@@ -1065,9 +1068,9 @@ dependencies = [
[[package]] [[package]]
name = "egui" name = "egui"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bab3b3572566257a497b5f87d2cccaf7f7f122d4b8b620cba0493becc7955e" checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"ahash", "ahash",
@@ -1081,9 +1084,9 @@ dependencies = [
[[package]] [[package]]
name = "egui-wgpu" name = "egui-wgpu"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba69456826abf572ed95b658b265c01f07a23d615d6f029eedc9ee5f13ddf788" checksum = "d00fd5d06d8405397e64a928fa0ef3934b3c30273ea7603e3dc4627b1f7a1a82"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytemuck", "bytemuck",
@@ -1100,9 +1103,9 @@ dependencies = [
[[package]] [[package]]
name = "egui-winit" name = "egui-winit"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642c749bf221b5a3ecae3144c98b837729d87b9fde6c39a6ad00f07b71dbee94" checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6"
dependencies = [ dependencies = [
"ahash", "ahash",
"arboard", "arboard",
@@ -1118,9 +1121,9 @@ dependencies = [
[[package]] [[package]]
name = "egui_extras" name = "egui_extras"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f1beb57a3c942fac2f058655188c79ac1cd200555e4f3684cd0c965ceb3a67" checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1"
dependencies = [ dependencies = [
"ahash", "ahash",
"egui", "egui",
@@ -1131,9 +1134,9 @@ dependencies = [
[[package]] [[package]]
name = "egui_glow" name = "egui_glow"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea182206896187f7a2fcc207a1573785fc31330cb245f6cebcf663ea933f8d20" checksum = "0e39bccc683cd43adab530d8f21a13eb91e80de10bcc38c3f1c16601b6f62b26"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytemuck", "bytemuck",
@@ -1154,9 +1157,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]] [[package]]
name = "emath" name = "emath"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af86c4efae11da2a3dcbb4afebd0e9ed1916345e8d187b4051d443c8bd79af93" checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"serde", "serde",
@@ -1215,7 +1218,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1236,7 +1239,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1247,14 +1250,14 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
name = "epaint" name = "epaint"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e11ec86a4d85e1350578ba20b2d89977ed937f3faab32e1c3ec81d20c1842" checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"ahash", "ahash",
@@ -1270,9 +1273,9 @@ dependencies = [
[[package]] [[package]]
name = "epaint_default_fonts" name = "epaint_default_fonts"
version = "0.29.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5202b64bef2b2c42a7f6e2e5b40fa83dd04aa61fdb08bfd116553adc149fe47a" checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
@@ -1400,7 +1403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide 0.8.0", "miniz_oxide",
] ]
[[package]] [[package]]
@@ -1467,7 +1470,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1554,7 +1557,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1662,8 +1665,8 @@ dependencies = [
"aho-corasick", "aho-corasick",
"bstr", "bstr",
"log", "log",
"regex-automata 0.4.7", "regex-automata 0.4.8",
"regex-syntax 0.8.4", "regex-syntax 0.8.5",
"serde", "serde",
] ]
@@ -1797,7 +1800,7 @@ checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"gpu-descriptor-types", "gpu-descriptor-types",
"hashbrown", "hashbrown 0.14.5",
] ]
[[package]] [[package]]
@@ -1819,6 +1822,12 @@ dependencies = [
"allocator-api2", "allocator-api2",
] ]
[[package]]
name = "hashbrown"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
[[package]] [[package]]
name = "hassle-rs" name = "hassle-rs"
version = "0.11.0" version = "0.11.0"
@@ -1909,9 +1918,9 @@ dependencies = [
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.9.4" version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]] [[package]]
name = "hyper" name = "hyper"
@@ -2018,12 +2027,12 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.15.0",
] ]
[[package]] [[package]]
@@ -2066,7 +2075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -2080,9 +2089,28 @@ dependencies = [
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.10.0" version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]] [[package]]
name = "is_ci" name = "is_ci"
@@ -2204,6 +2232,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "libmimalloc-sys"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.0.2" version = "0.0.2"
@@ -2223,7 +2261,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"libc", "libc",
"redox_syscall 0.5.6", "redox_syscall 0.5.7",
] ]
[[package]] [[package]]
@@ -2260,7 +2298,7 @@ version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.14.5",
] ]
[[package]] [[package]]
@@ -2320,6 +2358,15 @@ dependencies = [
"paste", "paste",
] ]
[[package]]
name = "mimalloc"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
dependencies = [
"libmimalloc-sys",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -2346,16 +2393,6 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "miniz_oxide"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
dependencies = [
"adler",
"simd-adler32",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.0" version = "0.8.0"
@@ -2363,6 +2400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32",
] ]
[[package]] [[package]]
@@ -2591,7 +2629,7 @@ dependencies = [
"proc-macro-crate 3.2.0", "proc-macro-crate 3.2.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -2823,13 +2861,14 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-cli" name = "objdiff-cli"
version = "2.1.0" version = "2.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argp", "argp",
"crossterm", "crossterm",
"enable-ansi-support", "enable-ansi-support",
"memmap2", "memmap2",
"mimalloc",
"objdiff-core", "objdiff-core",
"prost", "prost",
"ratatui", "ratatui",
@@ -2844,10 +2883,11 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-core" name = "objdiff-core"
version = "2.1.0" version = "2.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arm-attr", "arm-attr",
"bimap",
"byteorder", "byteorder",
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
@@ -2883,7 +2923,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-gui" name = "objdiff-gui"
version = "2.1.0" version = "2.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2904,6 +2944,7 @@ dependencies = [
"log", "log",
"notify", "notify",
"objdiff-core", "objdiff-core",
"open",
"path-slash", "path-slash",
"png", "png",
"pollster", "pollster",
@@ -2937,9 +2978,23 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
dependencies = [
"portable-atomic",
]
[[package]]
name = "open"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]] [[package]]
name = "openssl" name = "openssl"
@@ -2964,7 +3019,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3049,7 +3104,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.6", "redox_syscall 0.5.7",
"smallvec", "smallvec",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@@ -3066,6 +3121,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]] [[package]]
name = "pathfinder_geometry" name = "pathfinder_geometry"
version = "0.5.1" version = "0.5.1"
@@ -3140,7 +3201,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3174,15 +3235,15 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "png" name = "png"
version = "0.17.13" version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"crc32fast", "crc32fast",
"fdeflate", "fdeflate",
"flate2", "flate2",
"miniz_oxide 0.7.4", "miniz_oxide",
] ]
[[package]] [[package]]
@@ -3208,9 +3269,9 @@ checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
@@ -3246,7 +3307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3310,7 +3371,7 @@ dependencies = [
"prost", "prost",
"prost-types", "prost-types",
"regex", "regex",
"syn 2.0.77", "syn 2.0.79",
"tempfile", "tempfile",
] ]
@@ -3324,7 +3385,7 @@ dependencies = [
"itertools", "itertools",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3528,9 +3589,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.6" version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
@@ -3548,14 +3609,14 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.6" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.4.7", "regex-automata 0.4.8",
"regex-syntax 0.8.4", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@@ -3569,13 +3630,13 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.7" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax 0.8.4", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@@ -3586,9 +3647,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.4" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "renderdoc-sys" name = "renderdoc-sys"
@@ -3598,9 +3659,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.7" version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -3757,19 +3818,18 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "2.1.3" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [ dependencies = [
"base64 0.22.1",
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
@@ -3911,7 +3971,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3922,7 +3982,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3945,7 +4005,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4181,7 +4241,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4212,9 +4272,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.77" version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -4242,9 +4302,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.12.0" version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
@@ -4279,7 +4339,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4460,7 +4520,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4540,7 +4600,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4592,9 +4652,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.15" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
@@ -4755,7 +4815,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -4789,7 +4849,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -5596,7 +5656,7 @@ dependencies = [
"proc-macro-crate 3.2.0", "proc-macro-crate 3.2.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
"zvariant_utils", "zvariant_utils",
] ]
@@ -5629,7 +5689,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -5661,7 +5721,7 @@ dependencies = [
"proc-macro-crate 3.2.0", "proc-macro-crate 3.2.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
"zvariant_utils", "zvariant_utils",
] ]
@@ -5673,5 +5733,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]

View File

@@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1 codegen-units = 1
[workspace.package] [workspace.package]
version = "2.1.0" version = "2.3.1"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -133,6 +133,13 @@
}, },
"metadata": { "metadata": {
"ref": "#/$defs/metadata" "ref": "#/$defs/metadata"
},
"symbol_mappings": {
"type": "object",
"description": "Manual symbol mappings from target to base.",
"additionalProperties": {
"type": "string"
}
} }
} }
}, },

View File

@@ -28,3 +28,6 @@ supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"

View File

@@ -102,26 +102,32 @@ pub fn run(args: Args) -> Result<()> {
let unit_path = let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok()); PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config.objects.iter_mut().find_map(|obj| { let Some(object) = project_config
if obj.name.as_deref() == Some(u) { .units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj); resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?; if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
resolve_paths(obj); None
})
if [&obj.base_path, &obj.target_path] else {
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
}) else {
bail!("Unit not found: {}", u) bail!("Unit not found: {}", u)
}; };
@@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> {
} else if let Some(symbol_name) = &args.symbol { } else if let Some(symbol_name) = &args.symbol {
let mut idx = None; let mut idx = None;
let mut count = 0usize; let mut count = 0usize;
for (i, obj) in project_config.objects.iter_mut().enumerate() { for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj); resolve_paths(obj);
if obj if obj
@@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> {
} }
match (count, idx) { match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name), (0, None) => bail!("Symbol not found: {}", symbol_name),
(1, Some(i)) => &mut project_config.objects[i], (1, Some(i)) => &mut project_config.units_mut()[i],
(2.., Some(_)) => bail!( (2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit", "Multiple instances of {} were found, try specifying a unit",
symbol_name symbol_name
@@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
None None
} }
#[allow(dead_code)] #[expect(dead_code)]
struct FunctionDiffUi { struct FunctionDiffUi {
relax_reloc_diffs: bool, relax_reloc_diffs: bool,
left_highlight: HighlightKind, left_highlight: HighlightKind,
@@ -758,7 +770,7 @@ impl FunctionDiffUi {
self.scroll_y += self.per_page / if half { 2 } else { 1 }; self.scroll_y += self.per_page / if half { 2 } else { 1 };
} }
#[allow(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
fn print_sym( fn print_sym(
&self, &self,
out: &mut Text<'static>, out: &mut Text<'static>,

View File

@@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
}; };
info!( info!(
"Generating report for {} units (using {} threads)", "Generating report for {} units (using {} threads)",
project.objects.len(), project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() } if args.deduplicate { 1 } else { rayon::current_num_threads() }
); );
@@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
let mut existing_functions: HashSet<String> = HashSet::new(); let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate { if args.deduplicate {
// If deduplicating, we need to run single-threaded // If deduplicating, we need to run single-threaded
for object in &mut project.objects { for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object( if let Some(unit) = report_object(
object, object,
project_dir, project_dir,
@@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
} else { } else {
let vec = project let vec = project
.objects .units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut() .par_iter_mut()
.map(|object| { .map(|object| {
report_object( report_object(
@@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect(); let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let mut categories = Vec::new(); let mut categories = Vec::new();
for category in &project.progress_categories { for category in project.progress_categories() {
categories.push(ReportCategory { categories.push(ReportCategory {
id: category.id.clone(), id: category.id.clone(),
name: category.name.clone(), name: category.name.clone(),
@@ -199,7 +201,7 @@ fn report_object(
.unwrap_or_default(), .unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated), auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
}; };
let mut measures = Measures::default(); let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![]; let mut sections = vec![];
let mut functions = vec![]; let mut functions = vec![];
@@ -280,6 +282,7 @@ fn report_object(
if metadata.complete.unwrap_or(false) { if metadata.complete.unwrap_or(false) {
measures.complete_code = measures.total_code; measures.complete_code = measures.total_code;
measures.complete_data = measures.total_data; measures.complete_data = measures.total_data;
measures.complete_units = 1;
} }
measures.calc_fuzzy_match_percent(); measures.calc_fuzzy_match_percent();
measures.calc_matched_percent(); measures.calc_matched_percent();

View File

@@ -2,6 +2,12 @@ mod argp_version;
mod cmd; mod cmd;
mod util; mod util;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
#[cfg(target_env = "musl")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr}; use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
use anyhow::{Error, Result}; use anyhow::{Error, Result};

View File

@@ -17,8 +17,8 @@ crate-type = ["cdylib", "rlib"]
[features] [features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"] all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
any-arch = [] # Implicit, used to check if any arch is enabled any-arch = ["bimap"] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"] config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"]
dwarf = ["gimli"] dwarf = ["gimli"]
mips = ["any-arch", "rabbitizer"] mips = ["any-arch", "rabbitizer"]
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
@@ -32,6 +32,7 @@ features = ["all"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
bimap = { version = "0.6", features = ["serde"], optional = true }
byteorder = "1.5" byteorder = "1.5"
filetime = "0.2" filetime = "0.2"
flagset = "0.4" flagset = "0.4"
@@ -60,7 +61,7 @@ gimli = { version = "0.31", default-features = false, features = ["read-all"], o
# ppc # ppc
cwdemangle = { version = "1.0", optional = true } cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "0.3", optional = true } cwextab = { version = "1.0.2", optional = true }
ppc750cl = { version = "0.3", optional = true } ppc750cl = { version = "0.3", optional = true }
# mips # mips

View File

@@ -32,6 +32,10 @@ message Measures {
uint64 complete_data = 13; uint64 complete_data = 13;
// Completed (or "linked") data percent // Completed (or "linked") data percent
float complete_data_percent = 14; float complete_data_percent = 14;
// Total number of units
uint32 total_units = 15;
// Completed (or "linked") units
uint32 complete_units = 16;
} }
// Project progress report // Project progress report

View File

@@ -8,9 +8,10 @@ use serde_json::error::Category;
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
pub const REPORT_VERSION: u32 = 1; pub const REPORT_VERSION: u32 = 2;
impl Report { impl Report {
/// Attempts to parse the report as binary protobuf or JSON.
pub fn parse(data: &[u8]) -> Result<Self> { pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() { if data.is_empty() {
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)); bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
@@ -25,6 +26,7 @@ impl Report {
Ok(report) Ok(report)
} }
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> { fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
match serde_json::from_slice::<Self>(bytes) { match serde_json::from_slice::<Self>(bytes) {
Ok(report) => Ok(report), Ok(report) => Ok(report),
@@ -43,16 +45,23 @@ impl Report {
} }
} }
/// Migrates the report to the latest version.
/// Fails if the report version is newer than supported.
pub fn migrate(&mut self) -> Result<()> { pub fn migrate(&mut self) -> Result<()> {
if self.version == 0 { if self.version == 0 {
self.migrate_v0()?; self.migrate_v0()?;
} }
if self.version == 1 {
self.migrate_v1()?;
}
if self.version != REPORT_VERSION { if self.version != REPORT_VERSION {
bail!("Unsupported report version: {}", self.version); bail!("Unsupported report version: {}", self.version);
} }
Ok(()) Ok(())
} }
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
/// to measures, and sets `progress_categories` in unit metadata.
fn migrate_v0(&mut self) -> Result<()> { fn migrate_v0(&mut self) -> Result<()> {
let Some(measures) = &mut self.measures else { let Some(measures) = &mut self.measures else {
bail!("Missing measures in report"); bail!("Missing measures in report");
@@ -61,15 +70,16 @@ impl Report {
let Some(unit_measures) = &mut unit.measures else { let Some(unit_measures) = &mut unit.measures else {
bail!("Missing measures in report unit"); bail!("Missing measures in report unit");
}; };
let Some(metadata) = &mut unit.metadata else { let mut complete = false;
bail!("Missing metadata in report unit"); if let Some(metadata) = &mut unit.metadata {
if metadata.module_name.is_some() || metadata.module_id.is_some() {
metadata.progress_categories = vec!["modules".to_string()];
} else {
metadata.progress_categories = vec!["dol".to_string()];
}
complete = metadata.complete.unwrap_or(false);
}; };
if metadata.module_name.is_some() || metadata.module_id.is_some() { if complete {
metadata.progress_categories = vec!["modules".to_string()];
} else {
metadata.progress_categories = vec!["dol".to_string()];
}
if metadata.complete.unwrap_or(false) {
unit_measures.complete_code = unit_measures.total_code; unit_measures.complete_code = unit_measures.total_code;
unit_measures.complete_data = unit_measures.total_data; unit_measures.complete_data = unit_measures.total_data;
unit_measures.complete_code_percent = 100.0; unit_measures.complete_code_percent = 100.0;
@@ -84,10 +94,42 @@ impl Report {
measures.complete_data += unit_measures.complete_data; measures.complete_data += unit_measures.complete_data;
} }
measures.calc_matched_percent(); measures.calc_matched_percent();
self.calculate_progress_categories();
self.version = 1; self.version = 1;
Ok(()) Ok(())
} }
/// Adds `total_units` and `complete_units` to measures.
fn migrate_v1(&mut self) -> Result<()> {
let Some(total_measures) = &mut self.measures else {
bail!("Missing measures in report");
};
for unit in &mut self.units {
let Some(measures) = &mut unit.measures else {
bail!("Missing measures in report unit");
};
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
let progress_categories =
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
measures.total_units = 1;
measures.complete_units = complete;
total_measures.total_units += 1;
total_measures.complete_units += complete;
for id in progress_categories {
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
let Some(measures) = &mut category.measures else {
bail!("Missing measures in category");
};
measures.total_units += 1;
measures.complete_units += complete;
}
}
}
self.version = 2;
Ok(())
}
/// Calculate progress categories based on unit metadata.
pub fn calculate_progress_categories(&mut self) { pub fn calculate_progress_categories(&mut self) {
for unit in &self.units { for unit in &self.units {
let Some(metadata) = unit.metadata.as_ref() else { let Some(metadata) = unit.metadata.as_ref() else {
@@ -242,6 +284,8 @@ impl AddAssign for Measures {
self.matched_functions += other.matched_functions; self.matched_functions += other.matched_functions;
self.complete_code += other.complete_code; self.complete_code += other.complete_code;
self.complete_data += other.complete_data; self.complete_data += other.complete_data;
self.total_units += other.total_units;
self.complete_units += other.complete_units;
} }
} }

View File

@@ -1,77 +1,100 @@
use std::{ use std::{
fs,
fs::File, fs::File,
io::Read, io::{BufReader, BufWriter, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime; use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
#[inline] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectConfig { pub struct ProjectConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>, pub min_version: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_make: Option<String>, pub custom_make: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_args: Option<Vec<String>>, pub custom_args: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_dir: Option<PathBuf>, pub target_dir: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_dir: Option<PathBuf>, pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_base: bool, pub build_base: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: bool, pub build_target: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>, pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")] #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub objects: Vec<ProjectObject>, pub units: Option<Vec<ProjectObject>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Vec<ProjectProgressCategory>, pub progress_categories: Option<Vec<ProjectProgressCategory>>,
} }
#[derive(Default, Clone, serde::Deserialize)] impl ProjectConfig {
#[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
self.units.get_or_insert_with(Vec::new)
}
#[inline]
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
self.progress_categories.as_deref().unwrap_or_default()
}
#[inline]
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new)
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObject { pub struct ProjectObject {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.reverse_fn_order")] #[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.complete")] #[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<ProjectObjectMetadata>, pub metadata: Option<ProjectObjectMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_mappings: Option<SymbolMappings>,
} }
#[derive(Default, Clone, serde::Deserialize)] pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObjectMetadata { pub struct ProjectObjectMetadata {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>, pub source_path: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<String>>, pub progress_categories: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_generated: Option<bool>, pub auto_generated: Option<bool>,
} }
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectProgressCategory { pub struct ProjectProgressCategory {
#[serde(default)] #[serde(default)]
pub id: String, pub id: String,
@@ -112,32 +135,36 @@ impl ProjectObject {
} }
pub fn complete(&self) -> Option<bool> { pub fn complete(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
} }
pub fn reverse_fn_order(&self) -> Option<bool> { pub fn reverse_fn_order(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
} }
pub fn hidden(&self) -> bool { pub fn hidden(&self) -> bool {
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false) self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
} }
pub fn source_path(&self) -> Option<&String> {
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
}
} }
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig { pub struct ScratchConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>, pub platform: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<String>, pub compiler: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub c_flags: Option<String>, pub c_flags: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_path: Option<PathBuf>, pub ctx_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: bool, pub build_ctx: Option<bool>,
} }
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"]; pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
@@ -150,13 +177,13 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo { pub struct ProjectConfigInfo {
pub path: PathBuf, pub path: PathBuf,
pub timestamp: FileTime, pub timestamp: Option<FileTime>,
} }
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> { pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() { for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename); let config_path = dir.join(filename);
let Ok(mut file) = File::open(&config_path) else { let Ok(file) = File::open(&config_path) else {
continue; continue;
}; };
let metadata = file.metadata(); let metadata = file.metadata();
@@ -165,9 +192,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
continue; continue;
} }
let ts = FileTime::from_last_modification_time(&metadata); let ts = FileTime::from_last_modification_time(&metadata);
let mut reader = BufReader::new(file);
let mut result = match filename.contains("json") { let mut result = match filename.contains("json") {
true => read_json_config(&mut file), true => read_json_config(&mut reader),
false => read_yml_config(&mut file), false => read_yml_config(&mut reader),
}; };
if let Ok(config) = &result { if let Ok(config) = &result {
// Validate min_version if present // Validate min_version if present
@@ -175,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
result = Err(e); result = Err(e);
} }
} }
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts })); return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
} }
} }
None None
} }
pub fn save_project_config(
config: &ProjectConfig,
info: &ProjectConfigInfo,
) -> Result<ProjectConfigInfo> {
if let Some(last_ts) = info.timestamp {
// Check if the file has changed since we last read it
if let Ok(metadata) = fs::metadata(&info.path) {
let ts = FileTime::from_last_modification_time(&metadata);
if ts != last_ts {
return Err(anyhow!("Config file has changed since last read"));
}
}
}
let mut writer =
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
match ext {
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
"yml" | "yaml" => {
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
}
_ => Err(anyhow!("Unknown config file extension: {ext}")),
}?;
let file = writer.into_inner().context("Failed to flush file")?;
let metadata = file.metadata().context("Failed to get file metadata")?;
let ts = FileTime::from_last_modification_time(&metadata);
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
}
fn validate_min_version(config: &ProjectConfig) -> Result<()> { fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) }; let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))

View File

@@ -41,7 +41,7 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
}); });
} }
resolve_branches(&mut diff); resolve_branches(&mut diff);
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None }) Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
} }
pub fn diff_code( pub fn diff_code(
@@ -67,7 +67,7 @@ pub fn diff_code(
right.arg_diff = result.right_args_diff; right.arg_diff = result.right_args_diff;
} }
let total = left_out.insts.len(); let total = left_out.insts.len().max(right_out.insts.len());
let percent = if diff_state.diff_count >= total { let percent = if diff_state.diff_count >= total {
0.0 0.0
} else { } else {
@@ -77,13 +77,13 @@ pub fn diff_code(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: left_diff, instructions: left_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: right_diff, instructions: right_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
@@ -211,7 +211,7 @@ fn arg_eq(
left_diff: &ObjInsDiff, left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff, right_diff: &ObjInsDiff,
) -> bool { ) -> bool {
return match left { match left {
ObjInsArg::PlainText(l) => match right { ObjInsArg::PlainText(l) => match right {
ObjInsArg::PlainText(r) => l == r, ObjInsArg::PlainText(r) => l == r,
_ => false, _ => false,
@@ -236,7 +236,7 @@ fn arg_eq(
left_diff.branch_to.as_ref().map(|b| b.ins_idx) left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx) == right_diff.branch_to.as_ref().map(|b| b.ins_idx)
} }
}; }
} }
#[derive(Default)] #[derive(Default)]

View File

@@ -20,13 +20,13 @@ pub fn diff_bss_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
@@ -34,7 +34,7 @@ pub fn diff_bss_symbol(
} }
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff { pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None } ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
} }
/// Compare the data sections of two object files. /// Compare the data sections of two object files.
@@ -158,13 +158,13 @@ pub fn diff_data_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },

View File

@@ -29,7 +29,7 @@ pub enum DiffText<'a> {
Eol, Eol,
} }
#[derive(Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum HighlightKind { pub enum HighlightKind {
#[default] #[default]
None, None,

View File

@@ -3,6 +3,7 @@ use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use crate::{ use crate::{
config::SymbolMappings,
diff::{ diff::{
code::{diff_code, no_diff_code, process_code_symbol}, code::{diff_code, no_diff_code, process_code_symbol},
data::{ data::{
@@ -161,6 +162,8 @@ pub struct DiffObjConfig {
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub space_between_args: bool, pub space_between_args: bool,
pub combine_data_sections: bool, pub combine_data_sections: bool,
#[serde(default)]
pub symbol_mappings: MappingConfig,
// x86 // x86
pub x86_formatter: X86Formatter, pub x86_formatter: X86Formatter,
// MIPS // MIPS
@@ -182,6 +185,7 @@ impl Default for DiffObjConfig {
relax_reloc_diffs: false, relax_reloc_diffs: false,
space_between_args: true, space_between_args: true,
combine_data_sections: false, combine_data_sections: false,
symbol_mappings: Default::default(),
x86_formatter: Default::default(), x86_formatter: Default::default(),
mips_abi: Default::default(), mips_abi: Default::default(),
mips_instr_category: Default::default(), mips_instr_category: Default::default(),
@@ -223,8 +227,10 @@ impl ObjSectionDiff {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ObjSymbolDiff { pub struct ObjSymbolDiff {
/// The symbol ref this object
pub symbol_ref: SymbolRef, pub symbol_ref: SymbolRef,
pub diff_symbol: Option<SymbolRef>, /// The symbol ref in the _other_ object that this symbol was diffed against
pub target_symbol: Option<SymbolRef>,
pub instructions: Vec<ObjInsDiff>, pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>, pub match_percent: Option<f32>,
} }
@@ -294,8 +300,13 @@ pub struct ObjInsBranchTo {
#[derive(Default)] #[derive(Default)]
pub struct ObjDiff { pub struct ObjDiff {
/// A list of all section diffs in the object.
pub sections: Vec<ObjSectionDiff>, pub sections: Vec<ObjSectionDiff>,
/// Common BSS symbols don't live in a section, so they're stored separately.
pub common: Vec<ObjSymbolDiff>, pub common: Vec<ObjSymbolDiff>,
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
/// that are being mapped to the other object.
pub mapping_symbols: Vec<ObjSymbolDiff>,
} }
impl ObjDiff { impl ObjDiff {
@@ -303,13 +314,14 @@ impl ObjDiff {
let mut result = Self { let mut result = Self {
sections: Vec::with_capacity(obj.sections.len()), sections: Vec::with_capacity(obj.sections.len()),
common: Vec::with_capacity(obj.common.len()), common: Vec::with_capacity(obj.common.len()),
mapping_symbols: vec![],
}; };
for (section_idx, section) in obj.sections.iter().enumerate() { for (section_idx, section) in obj.sections.iter().enumerate() {
let mut symbols = Vec::with_capacity(section.symbols.len()); let mut symbols = Vec::with_capacity(section.symbols.len());
for (symbol_idx, _) in section.symbols.iter().enumerate() { for (symbol_idx, _) in section.symbols.iter().enumerate() {
symbols.push(ObjSymbolDiff { symbols.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx, symbol_idx }, symbol_ref: SymbolRef { section_idx, symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@@ -328,7 +340,7 @@ impl ObjDiff {
for (symbol_idx, _) in obj.common.iter().enumerate() { for (symbol_idx, _) in obj.common.iter().enumerate() {
result.common.push(ObjSymbolDiff { result.common.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx }, symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@@ -378,7 +390,7 @@ pub fn diff_objs(
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> { ) -> Result<DiffObjsResult> {
let symbol_matches = matching_symbols(left, right, prev)?; let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
let section_matches = matching_sections(left, right)?; let section_matches = matching_sections(left, right)?;
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p))); let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
@@ -529,6 +541,17 @@ pub fn diff_objs(
} }
} }
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
(right.as_mut(), left.as_mut())
{
if let Some(right_name) = &config.symbol_mappings.selecting_left {
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
}
if let Some(left_name) = &config.symbol_mappings.selecting_right {
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
}
}
Ok(DiffObjsResult { Ok(DiffObjsResult {
left: left.map(|(_, o)| o), left: left.map(|(_, o)| o),
right: right.map(|(_, o)| o), right: right.map(|(_, o)| o),
@@ -536,6 +559,63 @@ pub fn diff_objs(
}) })
} }
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
/// symbols in the other object that match the selected symbol's section and kind. This allows
/// us to display match percentages for all symbols in the other object that could be selected.
fn generate_mapping_symbols(
base_obj: &ObjInfo,
base_name: &str,
target_obj: &ObjInfo,
target_out: &mut ObjDiff,
config: &DiffObjConfig,
) -> Result<()> {
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
return Ok(());
};
let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref);
let Some(base_section) = base_section else {
return Ok(());
};
let base_code = match base_section.kind {
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
_ => None,
};
for (target_section_index, target_section) in
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
{
for (target_symbol_index, _target_symbol) in
target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind)
{
let target_symbol_ref =
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
match base_section.kind {
ObjSectionKind::Code => {
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
let (left_diff, _right_diff) = diff_code(
&target_code,
base_code.as_ref().unwrap(),
target_symbol_ref,
base_symbol_ref,
config,
)?;
target_out.mapping_symbols.push(left_diff);
}
ObjSectionKind::Data => {
let (left_diff, _right_diff) =
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
target_out.mapping_symbols.push(left_diff);
}
ObjSectionKind::Bss => {
let (left_diff, _right_diff) =
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
target_out.mapping_symbols.push(left_diff);
}
}
}
}
Ok(())
}
#[derive(Copy, Clone, Eq, PartialEq)] #[derive(Copy, Clone, Eq, PartialEq)]
struct SymbolMatch { struct SymbolMatch {
left: Option<SymbolRef>, left: Option<SymbolRef>,
@@ -551,19 +631,115 @@ struct SectionMatch {
section_kind: ObjSectionKind, section_kind: ObjSectionKind,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
pub struct MappingConfig {
/// Manual symbol mappings
pub mappings: SymbolMappings,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
}
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn apply_symbol_mappings(
left: &ObjInfo,
right: &ObjInfo,
mapping_config: &MappingConfig,
left_used: &mut HashSet<SymbolRef>,
right_used: &mut HashSet<SymbolRef>,
matches: &mut Vec<SymbolMatch>,
) -> Result<()> {
// If we're selecting a symbol to use as a comparison, mark it as used
// This ensures that we don't match it to another symbol at any point
if let Some(left_name) = &mapping_config.selecting_left {
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
left_used.insert(left_symbol);
}
}
if let Some(right_name) = &mapping_config.selecting_right {
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
right_used.insert(right_symbol);
}
}
// Apply manual symbol mappings
for (left_name, right_name) in &mapping_config.mappings {
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
continue;
};
if left_used.contains(&left_symbol) {
continue;
}
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
continue;
};
if right_used.contains(&right_symbol) {
continue;
}
let left_section = &left.sections[left_symbol.section_idx];
let right_section = &right.sections[right_symbol.section_idx];
if left_section.kind != right_section.kind {
log::warn!(
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
left_name,
left_section.kind,
right_name,
right_section.kind
);
continue;
}
matches.push(SymbolMatch {
left: Some(left_symbol),
right: Some(right_symbol),
prev: None, // TODO
section_kind: left_section.kind,
});
left_used.insert(left_symbol);
right_used.insert(right_symbol);
}
Ok(())
}
/// Find matching symbols between each object. /// Find matching symbols between each object.
fn matching_symbols( fn matching_symbols(
left: Option<&ObjInfo>, left: Option<&ObjInfo>,
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
mappings: &MappingConfig,
) -> Result<Vec<SymbolMatch>> { ) -> Result<Vec<SymbolMatch>> {
let mut matches = Vec::new(); let mut matches = Vec::new();
let mut left_used = HashSet::new();
let mut right_used = HashSet::new(); let mut right_used = HashSet::new();
if let Some(left) = left { if let Some(left) = left {
if let Some(right) = right {
apply_symbol_mappings(
left,
right,
mappings,
&mut left_used,
&mut right_used,
&mut matches,
)?;
}
for (section_idx, section) in left.sections.iter().enumerate() { for (section_idx, section) in left.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() { for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx, symbol_idx };
if left_used.contains(&symbol_ref) {
continue;
}
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx, symbol_idx }), left: Some(symbol_ref),
right: find_symbol(right, symbol, section, Some(&right_used)), right: find_symbol(right, symbol, section, Some(&right_used)),
prev: find_symbol(prev, symbol, section, None), prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind, section_kind: section.kind,
@@ -575,8 +751,12 @@ fn matching_symbols(
} }
} }
for (symbol_idx, symbol) in left.common.iter().enumerate() { for (symbol_idx, symbol) in left.common.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx: left.sections.len(), symbol_idx };
if left_used.contains(&symbol_ref) {
continue;
}
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }), left: Some(symbol_ref),
right: find_common_symbol(right, symbol), right: find_common_symbol(right, symbol),
prev: find_common_symbol(prev, symbol), prev: find_common_symbol(prev, symbol),
section_kind: ObjSectionKind::Bss, section_kind: ObjSectionKind::Bss,

View File

@@ -112,6 +112,15 @@ pub struct ObjIns {
pub orig: Option<String>, pub orig: Option<String>,
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum ObjSymbolKind {
#[default]
Unknown,
Function,
Object,
Section,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ObjSymbol { pub struct ObjSymbol {
pub name: String, pub name: String,
@@ -120,6 +129,7 @@ pub struct ObjSymbol {
pub section_address: u64, pub section_address: u64,
pub size: u64, pub size: u64,
pub size_known: bool, pub size_known: bool,
pub kind: ObjSymbolKind,
pub flags: ObjSymbolFlagSet, pub flags: ObjSymbolFlagSet,
pub addend: i64, pub addend: i64,
/// Original virtual address (from .note.split section) /// Original virtual address (from .note.split section)

View File

@@ -23,6 +23,7 @@ use crate::{
obj::{ obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION}, split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
ObjSymbolKind,
}, },
util::{read_u16, read_u32}, util::{read_u16, read_u32},
}; };
@@ -94,6 +95,13 @@ fn to_obj_symbol(
}) })
.unwrap_or(&[]); .unwrap_or(&[]);
let kind = match symbol.kind() {
SymbolKind::Text => ObjSymbolKind::Function,
SymbolKind::Data => ObjSymbolKind::Object,
SymbolKind::Section => ObjSymbolKind::Section,
_ => ObjSymbolKind::Unknown,
};
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: name.to_string(),
demangled_name, demangled_name,
@@ -101,6 +109,7 @@ fn to_obj_symbol(
section_address, section_address,
size: symbol.size(), size: symbol.size(),
size_known: symbol.size() != 0, size_known: symbol.size() != 0,
kind,
flags, flags,
addend, addend,
virtual_address, virtual_address,
@@ -152,26 +161,23 @@ fn symbols_by_section(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
section: &ObjSection, section: &ObjSection,
section_symbols: &[Symbol<'_, '_>],
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>, name_counts: &mut HashMap<String, u32>,
) -> Result<Vec<ObjSymbol>> { ) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new(); let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() { for symbol in section_symbols {
if symbol.kind() == SymbolKind::Section { if symbol.kind() == SymbolKind::Section {
continue; continue;
} }
if let Some(index) = symbol.section().index() { if symbol.is_local() && section.kind == ObjSectionKind::Code {
if index.0 == section.orig_index { // TODO strip local syms in diff?
if symbol.is_local() && section.kind == ObjSectionKind::Code { let name = symbol.name().context("Failed to process symbol name")?;
// TODO strip local syms in diff? if symbol.size() == 0 || name.starts_with("lbl_") {
let name = symbol.name().context("Failed to process symbol name")?; continue;
if symbol.size() == 0 || name.starts_with("lbl_") {
continue;
}
}
result.push(to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)?);
} }
} }
result.push(to_obj_symbol(arch, obj_file, symbol, 0, split_meta)?);
} }
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size))); result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
let mut iter = result.iter_mut().peekable(); let mut iter = result.iter_mut().peekable();
@@ -182,6 +188,13 @@ fn symbols_by_section(
} else { } else {
symbol.size = (section.address + section.size) - symbol.address; symbol.size = (section.address + section.size) - symbol.address;
} }
// Set symbol kind if we ended up with a non-zero size
if symbol.kind == ObjSymbolKind::Unknown && symbol.size > 0 {
symbol.kind = match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
};
}
} }
} }
if result.is_empty() { if result.is_empty() {
@@ -199,6 +212,10 @@ fn symbols_by_section(
section_address: 0, section_address: 0,
size: section.size, size: section.size,
size_known: true, size_known: true,
kind: match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
},
flags: Default::default(), flags: Default::default(),
addend: 0, addend: 0,
virtual_address: None, virtual_address: None,
@@ -221,47 +238,72 @@ fn common_symbols(
.collect::<Result<Vec<ObjSymbol>>>() .collect::<Result<Vec<ObjSymbol>>>()
} }
const LOW_PRIORITY_SYMBOLS: &[&str] =
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
fn best_symbol<'r, 'data, 'file>(
symbols: &'r [Symbol<'data, 'file>],
address: u64,
) -> Option<&'r Symbol<'data, 'file>> {
let closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
Ok(index) => Some(index),
Err(index) => index.checked_sub(1),
}?;
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
for symbol in symbols.iter().skip(closest_symbol_index) {
if symbol.address() > address {
break;
}
if symbol.kind() == SymbolKind::Section
|| (symbol.size() > 0 && (symbol.address() + symbol.size()) <= address)
{
continue;
}
// TODO priority ranking with visibility, etc
if let Some(best) = best_symbol {
if LOW_PRIORITY_SYMBOLS.contains(&best.name().unwrap_or_default())
&& !LOW_PRIORITY_SYMBOLS.contains(&symbol.name().unwrap_or_default())
{
best_symbol = Some(symbol);
}
} else {
best_symbol = Some(symbol);
}
}
best_symbol
}
fn find_section_symbol( fn find_section_symbol(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
section_symbols: &[Symbol<'_, '_>],
target: &Symbol<'_, '_>, target: &Symbol<'_, '_>,
address: u64, address: u64,
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> { ) -> Result<ObjSymbol> {
if let Some(symbol) = best_symbol(section_symbols, address) {
return to_obj_symbol(
arch,
obj_file,
symbol,
address as i64 - symbol.address() as i64,
split_meta,
);
}
// Fallback to section symbol
let section_index = let section_index =
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?; target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
let section = obj_file.section_by_index(section_index)?; let section = obj_file.section_by_index(section_index)?;
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
for symbol in obj_file.symbols() {
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
continue;
}
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
if symbol.address() < address
&& symbol.size() != 0
&& (closest_symbol.is_none()
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
{
closest_symbol = Some(symbol);
}
continue;
}
return to_obj_symbol(arch, obj_file, &symbol, 0, split_meta);
}
let (name, offset) = closest_symbol
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
.or_else(|| section.name().map(|n| (n, section.address())).ok())
.unwrap_or(("<unknown>", 0));
let offset_addr = address - offset;
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: section.name()?.to_string(),
demangled_name: None, demangled_name: None,
address: offset, address: section.address(),
section_address: address - section.address(), section_address: 0,
size: 0, size: 0,
size_known: false, size_known: false,
kind: ObjSymbolKind::Section,
flags: Default::default(), flags: Default::default(),
addend: offset_addr as i64, addend: address as i64 - section.address() as i64,
virtual_address: None, virtual_address: None,
original_index: None, original_index: None,
bytes: Vec::new(), bytes: Vec::new(),
@@ -272,6 +314,7 @@ fn relocations_by_section(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
section: &ObjSection, section: &ObjSection,
section_symbols: &[Symbol<'_, '_>],
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjReloc>> { ) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?; let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
@@ -315,7 +358,14 @@ fn relocations_by_section(
} }
SymbolKind::Section => { SymbolKind::Section => {
ensure!(addend >= 0, "Negative addend in reloc: {addend}"); ensure!(addend >= 0, "Negative addend in reloc: {addend}");
find_section_symbol(arch, obj_file, &symbol, addend as u64, split_meta) find_section_symbol(
arch,
obj_file,
section_symbols,
&symbol,
addend as u64,
split_meta,
)
} }
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")), kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
}?; }?;
@@ -539,6 +589,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
section_address: (symbol.section_address as i64 + address_change).try_into()?, section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size, size: symbol.size,
size_known: symbol.size_known, size_known: symbol.size_known,
kind: symbol.kind,
flags: symbol.flags, flags: symbol.flags,
addend: symbol.addend, addend: symbol.addend,
virtual_address: if let Some(virtual_address) = symbol.virtual_address { virtual_address: if let Some(virtual_address) = symbol.virtual_address {
@@ -650,15 +701,26 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?; let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
let mut name_counts: HashMap<String, u32> = HashMap::new(); let mut name_counts: HashMap<String, u32> = HashMap::new();
for section in &mut sections { for section in &mut sections {
let mut symbols = obj_file
.symbols()
.filter(|s| s.section_index() == Some(SectionIndex(section.orig_index)))
.collect::<Vec<_>>();
symbols.sort_by_key(|s| s.address());
section.symbols = symbols_by_section( section.symbols = symbols_by_section(
arch.as_ref(), arch.as_ref(),
&obj_file, &obj_file,
section, section,
&symbols,
split_meta.as_ref(), split_meta.as_ref(),
&mut name_counts, &mut name_counts,
)?; )?;
section.relocations = section.relocations = relocations_by_section(
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?; arch.as_ref(),
&obj_file,
section,
&symbols,
split_meta.as_ref(),
)?;
} }
if config.combine_data_sections { if config.combine_data_sections {
combine_data_sections(&mut sections)?; combine_data_sections(&mut sections)?;

View File

@@ -29,7 +29,7 @@ bytes = "1.7"
cfg-if = "1.0" cfg-if = "1.0"
const_format = "0.2" const_format = "0.2"
cwdemangle = "1.0" cwdemangle = "1.0"
cwextab = "0.3.1" cwextab = "1.0.2"
dirs = "5.0" dirs = "5.0"
egui = "0.29" egui = "0.29"
egui_extras = "0.29" egui_extras = "0.29"
@@ -40,9 +40,10 @@ globset = { version = "0.4", features = ["serde1"] }
log = "0.4" log = "0.4"
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" } notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3"
png = "0.17" png = "0.17"
pollster = "0.3" pollster = "0.3"
regex = "1.10" regex = "1.11"
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal'] rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0" rlwinmdec = "1.0"
ron = "0.8" ron = "0.8"
@@ -50,7 +51,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
shell-escape = "0.1" shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
tempfile = "3.12" tempfile = "3.13"
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui # Keep version in sync with egui

View File

@@ -7,6 +7,7 @@ use std::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock, Arc, Mutex, RwLock,
}, },
time::Instant,
}; };
use filetime::FileTime; use filetime::FileTime;
@@ -14,7 +15,8 @@ use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher}; use notify::{RecursiveMode, Watcher};
use objdiff_core::{ use objdiff_core::{
config::{ config::{
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS, build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
}; };
@@ -39,13 +41,12 @@ use crate::{
frame_history::FrameHistory, frame_history::FrameHistory,
function_diff::function_diff_ui, function_diff::function_diff_ui,
graphics::{graphics_window, GraphicsConfig, GraphicsViewState}, graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::jobs_ui, jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState}, rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewState, View}, symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
}, },
}; };
#[derive(Default)]
pub struct ViewState { pub struct ViewState {
pub jobs: JobQueue, pub jobs: JobQueue,
pub config_state: ConfigViewState, pub config_state: ConfigViewState,
@@ -61,10 +62,35 @@ pub struct ViewState {
pub show_arch_config: bool, pub show_arch_config: bool,
pub show_debug: bool, pub show_debug: bool,
pub show_graphics: bool, pub show_graphics: bool,
pub show_jobs: bool,
pub show_side_panel: bool,
}
impl Default for ViewState {
fn default() -> Self {
Self {
jobs: Default::default(),
config_state: Default::default(),
demangle_state: Default::default(),
rlwinm_decode_state: Default::default(),
diff_state: Default::default(),
graphics_state: Default::default(),
frame_history: Default::default(),
show_appearance_config: false,
show_demangle: false,
show_rlwinm_decode: false,
show_project_config: false,
show_arch_config: false,
show_debug: false,
show_graphics: false,
show_jobs: false,
show_side_panel: true,
}
}
} }
/// The configuration for a single object file. /// The configuration for a single object file.
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig { pub struct ObjectConfig {
pub name: String, pub name: String,
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
@@ -72,6 +98,24 @@ pub struct ObjectConfig {
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>,
#[serde(default)]
pub symbol_mappings: SymbolMappings,
}
impl From<&ProjectObject> for ObjectConfig {
fn from(object: &ProjectObject) -> Self {
Self {
name: object.name().to_string(),
target_path: object.target_path.clone(),
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(),
scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
}
}
} }
#[inline] #[inline]
@@ -82,6 +126,46 @@ fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
} }
pub struct AppState {
pub config: AppConfig,
pub objects: Vec<ProjectObject>,
pub object_nodes: Vec<ProjectObjectNode>,
pub watcher_change: bool,
pub config_change: bool,
pub obj_change: bool,
pub queue_build: bool,
pub queue_reload: bool,
pub current_project_config: Option<ProjectConfig>,
pub project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
pub config_error: Option<String>,
}
impl Default for AppState {
fn default() -> Self {
Self {
config: Default::default(),
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
queue_reload: false,
current_project_config: None,
project_config_info: None,
last_mod_check: Instant::now(),
selecting_left: None,
selecting_right: None,
config_error: None,
}
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)] #[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig { pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455 // TODO: https://github.com/ron-rs/ron/pull/455
@@ -116,23 +200,6 @@ pub struct AppConfig {
pub recent_projects: Vec<PathBuf>, pub recent_projects: Vec<PathBuf>,
#[serde(default)] #[serde(default)]
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
#[serde(skip)]
pub objects: Vec<ProjectObject>,
#[serde(skip)]
pub object_nodes: Vec<ProjectObjectNode>,
#[serde(skip)]
pub watcher_change: bool,
#[serde(skip)]
pub config_change: bool,
#[serde(skip)]
pub obj_change: bool,
#[serde(skip)]
pub queue_build: bool,
#[serde(skip)]
pub queue_reload: bool,
#[serde(skip)]
pub project_config_info: Option<ProjectConfigInfo>,
} }
impl Default for AppConfig { impl Default for AppConfig {
@@ -153,67 +220,184 @@ impl Default for AppConfig {
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(), watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
recent_projects: vec![], recent_projects: vec![],
diff_obj_config: Default::default(), diff_obj_config: Default::default(),
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
queue_reload: false,
project_config_info: None,
} }
} }
} }
impl AppConfig { impl AppState {
pub fn set_project_dir(&mut self, path: PathBuf) { pub fn set_project_dir(&mut self, path: PathBuf) {
self.recent_projects.retain(|p| p != &path); self.config.recent_projects.retain(|p| p != &path);
if self.recent_projects.len() > 9 { if self.config.recent_projects.len() > 9 {
self.recent_projects.truncate(9); self.config.recent_projects.truncate(9);
} }
self.recent_projects.insert(0, path.clone()); self.config.recent_projects.insert(0, path.clone());
self.project_dir = Some(path); self.config.project_dir = Some(path);
self.target_obj_dir = None; self.config.target_obj_dir = None;
self.base_obj_dir = None; self.config.base_obj_dir = None;
self.selected_obj = None; self.config.selected_obj = None;
self.build_target = false; self.config.build_target = false;
self.objects.clear(); self.objects.clear();
self.object_nodes.clear(); self.object_nodes.clear();
self.watcher_change = true; self.watcher_change = true;
self.config_change = true; self.config_change = true;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.current_project_config = None;
self.project_config_info = None; self.project_config_info = None;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_target_obj_dir(&mut self, path: PathBuf) { pub fn set_target_obj_dir(&mut self, path: PathBuf) {
self.target_obj_dir = Some(path); self.config.target_obj_dir = Some(path);
self.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_base_obj_dir(&mut self, path: PathBuf) { pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.base_obj_dir = Some(path); self.config.base_obj_dir = Some(path);
self.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_selected_obj(&mut self, object: ObjectConfig) { pub fn set_selected_obj(&mut self, config: ObjectConfig) {
self.selected_obj = Some(object); let mut unit_changed = true;
if let Some(existing) = self.config.selected_obj.as_ref() {
if existing == &config {
// Don't reload the object if there were no changes
return;
}
if existing.name == config.name {
unit_changed = false;
}
}
self.config.selected_obj = Some(config);
if unit_changed {
self.obj_change = true;
self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} else {
self.queue_build = true;
}
}
pub fn clear_selected_obj(&mut self) {
self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
}
pub fn set_selecting_left(&mut self, right: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_right(right);
self.selecting_left = Some(right.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_selecting_right(&mut self, left: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_left(left);
self.selecting_right = Some(left.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
let Some(object) = self.config.selected_obj.as_mut() else {
log::warn!("No selected object");
return;
};
self.selecting_left = None;
self.selecting_right = None;
if left == right {
object.symbol_mappings.remove_by_left(&left);
object.symbol_mappings.remove_by_right(&right);
} else {
object.symbol_mappings.insert(left.clone(), right.clone());
}
self.queue_reload = true;
self.save_config();
}
pub fn clear_selection(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
self.queue_reload = true;
}
pub fn clear_mappings(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
if let Some(object) = self.config.selected_obj.as_mut() {
object.symbol_mappings.clear();
}
self.queue_reload = true;
self.save_config();
}
pub fn is_selecting_symbol(&self) -> bool {
self.selecting_left.is_some() || self.selecting_right.is_some()
}
pub fn save_config(&mut self) {
let (Some(config), Some(info)) =
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
else {
return;
};
// Update the project config with the current state
if let Some(object) = self.config.selected_obj.as_ref() {
if let Some(existing) = config.units.as_mut().and_then(|v| {
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
}) {
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
if let Some(existing) =
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
{
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
}
// Save the updated project config
match save_project_config(config, info) {
Ok(new_info) => *info = new_info,
Err(e) => {
log::error!("Failed to save project config: {e}");
self.config_error = Some(format!("Failed to save project config: {e}"));
}
}
} }
} }
pub type AppConfigRef = Arc<RwLock<AppConfig>>; pub type AppStateRef = Arc<RwLock<AppState>>;
#[derive(Default)] #[derive(Default)]
pub struct App { pub struct App {
appearance: Appearance, appearance: Appearance,
view_state: ViewState, view_state: ViewState,
config: AppConfigRef, state: AppStateRef,
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>, watcher: Option<notify::RecommendedWatcher>,
app_path: Option<PathBuf>, app_path: Option<PathBuf>,
@@ -241,16 +425,17 @@ impl App {
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) { if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
app.appearance = appearance; app.appearance = appearance;
} }
if let Some(mut config) = deserialize_config(storage) { if let Some(config) = deserialize_config(storage) {
if config.project_dir.is_some() { let mut state = AppState { config, ..Default::default() };
config.config_change = true; if state.config.project_dir.is_some() {
config.watcher_change = true; state.config_change = true;
state.watcher_change = true;
} }
if config.selected_obj.is_some() { if state.config.selected_obj.is_some() {
config.queue_build = true; state.queue_build = true;
} }
app.view_state.config_state.queue_check_update = config.auto_update_check; app.view_state.config_state.queue_check_update = state.config.auto_update_check;
app.config = Arc::new(RwLock::new(config)); app.state = Arc::new(RwLock::new(state));
} }
} }
app.appearance.init_fonts(&cc.egui_ctx); app.appearance.init_fonts(&cc.egui_ctx);
@@ -336,81 +521,93 @@ impl App {
jobs.results.append(&mut results); jobs.results.append(&mut results);
jobs.clear_finished(); jobs.clear_finished();
diff_state.pre_update(jobs, &self.config); diff_state.pre_update(jobs, &self.state);
config_state.pre_update(jobs, &self.config); config_state.pre_update(jobs, &self.state);
debug_assert!(jobs.results.is_empty()); debug_assert!(jobs.results.is_empty());
} }
fn post_update(&mut self, ctx: &egui::Context) { fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
self.appearance.post_update(ctx); self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state; let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.config); config_state.post_update(ctx, jobs, &self.state);
diff_state.post_update(ctx, jobs, &self.config); diff_state.post_update(action, ctx, jobs, &self.state);
let Ok(mut config) = self.config.write() else { let Ok(mut state) = self.state.write() else {
return; return;
}; };
let config = &mut *config; let state = &mut *state;
if let Some(info) = &config.project_config_info { let mut mod_check = false;
if file_modified(&info.path, info.timestamp) { if state.last_mod_check.elapsed().as_millis() >= 500 {
config.config_change = true; state.last_mod_check = Instant::now();
} mod_check = true;
} }
if config.config_change { if mod_check {
config.config_change = false; if let Some(info) = &state.project_config_info {
match load_project_config(config) { if let Some(last_ts) = info.timestamp {
Ok(()) => config_state.load_error = None, if file_modified(&info.path, last_ts) {
Err(e) => { state.config_change = true;
log::error!("Failed to load project config: {e}");
config_state.load_error = Some(format!("{e}"));
}
}
}
if config.watcher_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
},
) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
config.watcher_change = false;
}
}
if config.obj_change {
*diff_state = Default::default();
if config.selected_obj.is_some() {
config.queue_build = true;
}
config.obj_change = false;
}
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
config.queue_build = true;
}
if let Some(result) = &diff_state.build {
if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
config.queue_reload = true;
} }
} }
} }
if let Some((obj, _)) = &result.second_obj { }
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) { if state.config_change {
config.queue_reload = true; state.config_change = false;
match load_project_config(state) {
Ok(()) => state.config_error = None,
Err(e) => {
log::error!("Failed to load project config: {e}");
state.config_error = Some(format!("{e}"));
}
}
}
if state.watcher_change {
drop(self.watcher.take());
if let Some(project_dir) = &state.config.project_dir {
match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
}) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
state.watcher_change = false;
}
}
if state.obj_change {
*diff_state = Default::default();
if state.config.selected_obj.is_some() {
state.queue_build = true;
}
state.obj_change = false;
}
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
state.queue_build = true;
}
if let Some(result) = &diff_state.build {
if mod_check {
if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
state.queue_reload = true;
}
}
}
if let Some((obj, _)) = &result.second_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
state.queue_reload = true;
}
} }
} }
} }
@@ -418,17 +615,20 @@ impl App {
// Don't clear `queue_build` if a build is running. A file may have been modified during // Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes. // the build, so we'll start another build after the current one finishes.
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) { if state.queue_build
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config))); && state.config.selected_obj.is_some()
config.queue_build = false; && !jobs.is_running(Job::ObjDiff)
config.queue_reload = false; {
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) { jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
let mut diff_config = ObjDiffConfig::from_config(config); state.queue_build = false;
state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_state(state);
// Don't build, just reload the current files // Don't build, just reload the current files
diff_config.build_base = false; diff_config.build_base = false;
diff_config.build_target = false; diff_config.build_target = false;
jobs.push(start_build(ctx, diff_config)); jobs.push(start_build(ctx, diff_config));
config.queue_reload = false; state.queue_reload = false;
} }
if graphics_state.should_relaunch { if graphics_state.should_relaunch {
@@ -453,7 +653,7 @@ impl eframe::App for App {
self.pre_update(ctx); self.pre_update(ctx);
let Self { config, appearance, view_state, .. } = self; let Self { state, appearance, view_state, .. } = self;
let ViewState { let ViewState {
jobs, jobs,
config_state, config_state,
@@ -469,12 +669,27 @@ impl eframe::App for App {
show_arch_config, show_arch_config,
show_debug, show_debug,
show_graphics, show_graphics,
show_jobs,
show_side_panel,
} = view_state; } = view_state;
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
let side_panel_available = diff_state.current_view == View::SymbolDiff;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
if ui
.add_enabled(
side_panel_available,
egui::Button::new(if *show_side_panel { "" } else { "" }),
)
.on_hover_text("Toggle side panel")
.clicked()
{
*show_side_panel = !*show_side_panel;
}
ui.separator();
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if ui.button("Debug…").clicked() { if ui.button("Debug…").clicked() {
@@ -485,8 +700,8 @@ impl eframe::App for App {
*show_project_config = !*show_project_config; *show_project_config = !*show_project_config;
ui.close_menu(); ui.close_menu();
} }
let recent_projects = if let Ok(guard) = config.read() { let recent_projects = if let Ok(guard) = state.read() {
guard.recent_projects.clone() guard.config.recent_projects.clone()
} else { } else {
vec![] vec![]
}; };
@@ -495,12 +710,12 @@ impl eframe::App for App {
} else { } else {
ui.menu_button("Recent Projects…", |ui| { ui.menu_button("Recent Projects…", |ui| {
if ui.button("Clear").clicked() { if ui.button("Clear").clicked() {
config.write().unwrap().recent_projects.clear(); state.write().unwrap().config.recent_projects.clear();
}; };
ui.separator(); ui.separator();
for path in recent_projects { for path in recent_projects {
if ui.button(format!("{}", path.display())).clicked() { if ui.button(format!("{}", path.display())).clicked() {
config.write().unwrap().set_project_dir(path); state.write().unwrap().set_project_dir(path);
ui.close_menu(); ui.close_menu();
} }
} }
@@ -533,12 +748,12 @@ impl eframe::App for App {
*show_arch_config = !*show_arch_config; *show_arch_config = !*show_arch_config;
ui.close_menu(); ui.close_menu();
} }
let mut config = config.write().unwrap(); let mut state = state.write().unwrap();
let response = ui let response = ui
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes") .checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
.on_hover_text("Automatically re-run the build & diff when files change."); .on_hover_text("Automatically re-run the build & diff when files change.");
if response.changed() { if response.changed() {
config.watcher_change = true; state.watcher_change = true;
}; };
ui.add_enabled( ui.add_enabled(
!diff_state.symbol_state.disable_reverse_fn_order, !diff_state.symbol_state.disable_reverse_fn_order,
@@ -554,7 +769,7 @@ impl eframe::App for App {
); );
if ui if ui
.checkbox( .checkbox(
&mut config.diff_obj_config.relax_reloc_diffs, &mut state.config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs", "Relax relocation diffs",
) )
.on_hover_text( .on_hover_text(
@@ -562,72 +777,78 @@ impl eframe::App for App {
) )
.changed() .changed()
{ {
config.queue_reload = true; state.queue_reload = true;
} }
if ui if ui
.checkbox( .checkbox(
&mut config.diff_obj_config.space_between_args, &mut state.config.diff_obj_config.space_between_args,
"Space between args", "Space between args",
) )
.changed() .changed()
{ {
config.queue_reload = true; state.queue_reload = true;
} }
if ui if ui
.checkbox( .checkbox(
&mut config.diff_obj_config.combine_data_sections, &mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections", "Combine data sections",
) )
.on_hover_text("Combines data sections with equal names.") .on_hover_text("Combines data sections with equal names.")
.changed() .changed()
{ {
config.queue_reload = true; state.queue_reload = true;
}
if ui.button("Clear custom symbol mappings").clicked() {
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
state.queue_reload = true;
} }
}); });
ui.separator();
if jobs_menu_ui(ui, jobs, appearance) {
*show_jobs = !*show_jobs;
}
}); });
}); });
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success); if side_panel_available {
if diff_state.current_view == View::FunctionDiff && build_success { egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
egui::CentralPanel::default().show(ctx, |ui| {
function_diff_ui(ui, diff_state, appearance);
});
} else if diff_state.current_view == View::DataDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
data_diff_ui(ui, diff_state, appearance);
});
} else if diff_state.current_view == View::ExtabDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
extab_diff_ui(ui, diff_state, appearance);
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| { egui::ScrollArea::both().show(ui, |ui| {
config_ui(ui, config, show_project_config, config_state, appearance); config_ui(ui, state, show_project_config, config_state, appearance);
jobs_ui(ui, jobs, appearance);
}); });
}); });
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, diff_state, appearance);
});
} }
project_window(ctx, config, show_project_config, config_state, appearance); let mut action = None;
egui::CentralPanel::default().show(ctx, |ui| {
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
action = if diff_state.current_view == View::FunctionDiff && build_success {
function_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::DataDiff && build_success {
data_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::ExtabDiff && build_success {
extab_diff_ui(ui, diff_state, appearance)
} else {
symbol_diff_ui(ui, diff_state, appearance)
};
});
project_window(ctx, state, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance); appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance); demangle_window(ctx, show_demangle, demangle_state, appearance);
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance); rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
arch_config_window(ctx, config, show_arch_config, appearance); arch_config_window(ctx, state, show_arch_config, appearance);
debug_window(ctx, show_debug, frame_history, appearance); debug_window(ctx, show_debug, frame_history, appearance);
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance); graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, appearance);
self.post_update(ctx); self.post_update(ctx, action);
} }
/// Called by the frame work to save state before shutdown. /// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) { fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() { if let Ok(state) = self.state.read() {
eframe::set_value(storage, CONFIG_KEY, &*config); eframe::set_value(storage, CONFIG_KEY, &state.config);
} }
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance); eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
} }

View File

@@ -2,6 +2,10 @@ use std::path::PathBuf;
use eframe::Storage; use eframe::Storage;
use globset::Glob; use globset::Glob;
use objdiff_core::{
config::ScratchConfig,
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
};
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
@@ -11,7 +15,7 @@ pub struct AppConfigVersion {
} }
impl Default for AppConfigVersion { impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 1 } } fn default() -> Self { Self { version: 2 } }
} }
/// Deserialize the AppConfig from storage, handling upgrades from older versions. /// Deserialize the AppConfig from storage, handling upgrades from older versions.
@@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?; let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) { match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version { Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str), 2 => from_str::<AppConfig>(&str),
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
_ => { _ => {
log::warn!("Unknown config version: {}", version.version); log::warn!("Unknown config version: {}", version.version);
None None
@@ -44,6 +49,180 @@ where T: serde::de::DeserializeOwned {
} }
} }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV1 {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: bool,
}
impl ScratchConfigV1 {
fn into_config(self) -> ScratchConfig {
ScratchConfig {
platform: self.platform,
compiler: self.compiler,
c_flags: self.c_flags,
ctx_path: self.ctx_path,
build_ctx: self.build_ctx.then_some(true),
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV1 {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV1>,
pub source_path: Option<String>,
}
impl ObjectConfigV1 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: self.target_path,
base_path: self.base_path,
reverse_fn_order: self.reverse_fn_order,
complete: self.complete,
scratch: self.scratch.map(|scratch| scratch.into_config()),
source_path: self.source_path,
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct DiffObjConfigV1 {
pub relax_reloc_diffs: bool,
#[serde(default = "bool_true")]
pub space_between_args: bool,
pub combine_data_sections: bool,
// x86
pub x86_formatter: X86Formatter,
// MIPS
pub mips_abi: MipsAbi,
pub mips_instr_category: MipsInstrCategory,
// ARM
pub arm_arch_version: ArmArchVersion,
pub arm_unified_syntax: bool,
pub arm_av_registers: bool,
pub arm_r9_usage: ArmR9Usage,
pub arm_sl_usage: bool,
pub arm_fp_usage: bool,
pub arm_ip_usage: bool,
}
impl Default for DiffObjConfigV1 {
fn default() -> Self {
Self {
relax_reloc_diffs: false,
space_between_args: true,
combine_data_sections: false,
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
arm_arch_version: Default::default(),
arm_unified_syntax: true,
arm_av_registers: false,
arm_r9_usage: Default::default(),
arm_sl_usage: false,
arm_fp_usage: false,
arm_ip_usage: false,
}
}
}
impl DiffObjConfigV1 {
fn into_config(self) -> DiffObjConfig {
DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: self.space_between_args,
combine_data_sections: self.combine_data_sections,
x86_formatter: self.x86_formatter,
mips_abi: self.mips_abi,
mips_instr_category: self.mips_instr_category,
arm_arch_version: self.arm_arch_version,
arm_unified_syntax: self.arm_unified_syntax,
arm_av_registers: self.arm_av_registers,
arm_r9_usage: self.arm_r9_usage,
arm_sl_usage: self.arm_sl_usage,
arm_fp_usage: self.arm_fp_usage,
arm_ip_usage: self.arm_ip_usage,
..Default::default()
}
}
}
#[inline]
fn bool_true() -> bool { true }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV1 {
pub version: u32,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
pub selected_wsl_distro: Option<String>,
#[serde(default)]
pub project_dir: Option<PathBuf>,
#[serde(default)]
pub target_obj_dir: Option<PathBuf>,
#[serde(default)]
pub base_obj_dir: Option<PathBuf>,
#[serde(default)]
pub selected_obj: Option<ObjectConfigV1>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default = "bool_true")]
pub rebuild_on_changes: bool,
#[serde(default)]
pub auto_update_check: bool,
#[serde(default)]
pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub diff_obj_config: DiffObjConfigV1,
}
impl AppConfigV1 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v1");
AppConfig {
custom_make: self.custom_make,
custom_args: self.custom_args,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_base: self.build_base,
build_target: self.build_target,
rebuild_on_changes: self.rebuild_on_changes,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
recent_projects: self.recent_projects,
diff_obj_config: self.diff_obj_config.into_config(),
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 { pub struct ObjectConfigV0 {
pub name: String, pub name: String,
@@ -59,8 +238,7 @@ impl ObjectConfigV0 {
target_path: Some(self.target_path), target_path: Some(self.target_path),
base_path: Some(self.base_path), base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: None, ..Default::default()
scratch: None,
} }
} }
} }

View File

@@ -4,11 +4,11 @@ use anyhow::Result;
use globset::Glob; use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS}; use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::AppConfig; use crate::app::{AppState, ObjectConfig};
#[derive(Clone)] #[derive(Clone)]
pub enum ProjectObjectNode { pub enum ProjectObjectNode {
File(String, Box<ProjectObject>), Unit(String, usize),
Dir(String, Vec<ProjectObjectNode>), Dir(String, Vec<ProjectObjectNode>),
} }
@@ -33,17 +33,18 @@ fn find_dir<'a>(
} }
fn build_nodes( fn build_nodes(
objects: &[ProjectObject], units: &mut [ProjectObject],
project_dir: &Path, project_dir: &Path,
target_obj_dir: Option<&Path>, target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>, base_obj_dir: Option<&Path>,
) -> Vec<ProjectObjectNode> { ) -> Vec<ProjectObjectNode> {
let mut nodes = vec![]; let mut nodes = vec![];
for object in objects { for (idx, unit) in units.iter_mut().enumerate() {
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
let mut out_nodes = &mut nodes; let mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name { let path = if let Some(name) = &unit.name {
Path::new(name) Path::new(name)
} else if let Some(path) = &object.path { } else if let Some(path) = &unit.path {
path path
} else { } else {
continue; continue;
@@ -56,38 +57,48 @@ fn build_nodes(
} }
} }
} }
let mut object = Box::new(object.clone());
object.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
let filename = path.file_name().unwrap().to_str().unwrap().to_string(); let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::File(filename, object)); out_nodes.push(ProjectObjectNode::Unit(filename, idx));
} }
nodes nodes
} }
pub fn load_project_config(config: &mut AppConfig) -> Result<()> { pub fn load_project_config(state: &mut AppState) -> Result<()> {
let Some(project_dir) = &config.project_dir else { let Some(project_dir) = &state.config.project_dir else {
return Ok(()); return Ok(());
}; };
if let Some((result, info)) = try_project_config(project_dir) { if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?; let project_config = result?;
config.custom_make = project_config.custom_make; state.config.custom_make = project_config.custom_make.clone();
config.custom_args = project_config.custom_args; state.config.custom_args = project_config.custom_args.clone();
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); state.config.target_obj_dir =
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); project_config.target_dir.as_deref().map(|p| project_dir.join(p));
config.build_base = project_config.build_base; state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
config.build_target = project_config.build_target; state.config.build_base = project_config.build_base.unwrap_or(true);
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| { state.config.build_target = project_config.build_target.unwrap_or(false);
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}); });
config.watcher_change = true; state.watcher_change = true;
config.objects = project_config.objects; state.objects = project_config.units.clone().unwrap_or_default();
config.object_nodes = build_nodes( state.object_nodes = build_nodes(
&config.objects, &mut state.objects,
project_dir, project_dir,
config.target_obj_dir.as_deref(), state.config.target_obj_dir.as_deref(),
config.base_obj_dir.as_deref(), state.config.base_obj_dir.as_deref(),
); );
config.project_config_info = Some(info); state.current_project_config = Some(project_config);
state.project_config_info = Some(info);
// Reload selected object
if let Some(selected_obj) = &state.config.selected_obj {
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
let config = ObjectConfig::from(obj);
state.set_selected_obj(config);
} else {
state.clear_selected_obj();
}
}
} }
Ok(()) Ok(())
} }

View File

@@ -39,7 +39,7 @@ impl CreateScratchConfig {
Ok(Self { Ok(Self {
build_config: BuildConfig::from_config(config), build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(), context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx, build_context: scratch_config.build_ctx.unwrap_or(false),
compiler: scratch_config.compiler.clone().unwrap_or_default(), compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(), platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),

View File

@@ -53,7 +53,7 @@ impl JobQueue {
} }
/// Returns whether any job is running. /// Returns whether any job is running.
#[allow(dead_code)] #[expect(dead_code)]
pub fn any_running(&self) -> bool { pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| { self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle { if let Some(handle) = &job.handle {
@@ -85,12 +85,15 @@ impl JobQueue {
/// Clears all finished jobs. /// Clears all finished jobs.
pub fn clear_finished(&mut self) { pub fn clear_finished(&mut self) {
self.jobs.retain(|job| { self.jobs.retain(|job| {
!(job.should_remove !(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
&& job.handle.is_none()
&& job.context.status.read().unwrap().error.is_none())
}); });
} }
/// Clears all errored jobs.
pub fn clear_errored(&mut self) {
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
}
/// Removes a job from the queue given its ID. /// Removes a job from the queue given its ID.
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); } pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
} }
@@ -107,7 +110,6 @@ pub struct JobState {
pub handle: Option<JoinHandle<JobResult>>, pub handle: Option<JoinHandle<JobResult>>,
pub context: JobContext, pub context: JobContext,
pub cancel: Sender<()>, pub cancel: Sender<()>,
pub should_remove: bool,
} }
#[derive(Default)] #[derive(Default)]
@@ -163,7 +165,7 @@ fn start_job(
}); });
let id = JOB_ID.fetch_add(1, Ordering::Relaxed); let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id); log::info!("Started job {}", id);
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true } JobState { id, kind, handle: Some(handle), context, cancel: tx }
} }
fn update_status( fn update_status(

View File

@@ -1,19 +1,18 @@
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command, process::Command,
str::from_utf8,
sync::mpsc::Receiver, sync::mpsc::Receiver,
}; };
use anyhow::{anyhow, Context, Error, Result}; use anyhow::{anyhow, Error, Result};
use objdiff_core::{ use objdiff_core::{
diff::{diff_objs, DiffObjConfig, ObjDiff}, diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
obj::{read, ObjInfo}, obj::{read, ObjInfo},
}; };
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::{ use crate::{
app::{AppConfig, ObjectConfig}, app::{AppConfig, AppState, ObjectConfig},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
}; };
@@ -61,16 +60,20 @@ pub struct ObjDiffConfig {
pub build_target: bool, pub build_target: bool,
pub selected_obj: Option<ObjectConfig>, pub selected_obj: Option<ObjectConfig>,
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
} }
impl ObjDiffConfig { impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self { pub(crate) fn from_state(state: &AppState) -> Self {
Self { Self {
build_config: BuildConfig::from_config(config), build_config: BuildConfig::from_config(&state.config),
build_base: config.build_base, build_base: state.config.build_base,
build_target: config.build_target, build_target: state.config.build_target,
selected_obj: config.selected_obj.clone(), selected_obj: state.config.selected_obj.clone(),
diff_obj_config: config.diff_obj_config.clone(), diff_obj_config: state.config.diff_obj_config.clone(),
selecting_left: state.selecting_left.clone(),
selecting_right: state.selecting_right.clone(),
} }
} }
} }
@@ -91,13 +94,6 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
..Default::default() ..Default::default()
}; };
}; };
match run_make_cmd(config, cwd, arg) {
Ok(status) => status,
Err(e) => BuildStatus { success: false, stderr: e.to_string(), ..Default::default() },
}
}
fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildStatus> {
let make = config.custom_make.as_deref().unwrap_or("make"); let make = config.custom_make.as_deref().unwrap_or("make");
let make_args = config.custom_args.as_deref().unwrap_or(&[]); let make_args = config.custom_args.as_deref().unwrap_or(&[]);
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -144,23 +140,38 @@ fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildSta
cmdline.push(' '); cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref()); cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
} }
let output = command.output().map_err(|e| anyhow!("Failed to execute build: {e}"))?; let output = match command.output() {
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?; Ok(output) => output,
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?; Err(e) => {
Ok(BuildStatus { return BuildStatus {
success: output.status.code().unwrap_or(-1) == 0, success: false,
cmdline, cmdline,
stdout: stdout.to_string(), stdout: Default::default(),
stderr: stderr.to_string(), stderr: e.to_string(),
}) };
}
};
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
let stdout = String::from_utf8(output.stdout)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
} }
fn run_build( fn run_build(
context: &JobContext, context: &JobContext,
cancel: Receiver<()>, cancel: Receiver<()>,
config: ObjDiffConfig, mut config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> { ) -> Result<Box<ObjDiffResult>> {
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?;
// Use the per-object symbol mappings, we don't set mappings globally
config.diff_obj_config.symbol_mappings = MappingConfig {
mappings: obj_config.symbol_mappings,
selecting_left: config.selecting_left,
selecting_right: config.selecting_right,
};
let project_dir = config let project_dir = config
.build_config .build_config
.project_dir .project_dir
@@ -189,36 +200,46 @@ fn run_build(
None None
}; };
let mut total = 3; let mut total = 1;
if config.build_target && target_path_rel.is_some() { if config.build_target && target_path_rel.is_some() {
total += 1; total += 1;
} }
if config.build_base && base_path_rel.is_some() { if config.build_base && base_path_rel.is_some() {
total += 1; total += 1;
} }
let first_status = match target_path_rel { if target_path_rel.is_some() {
total += 1;
}
if base_path_rel.is_some() {
total += 1;
}
let mut step_idx = 0;
let mut first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => { Some(target_path_rel) if config.build_target => {
update_status( update_status(
context, context,
format!("Building target {}", target_path_rel.display()), format!("Building target {}", target_path_rel.display()),
0, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1;
run_make(&config.build_config, target_path_rel) run_make(&config.build_config, target_path_rel)
} }
_ => BuildStatus::default(), _ => BuildStatus::default(),
}; };
let second_status = match base_path_rel { let mut second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => { Some(base_path_rel) if config.build_base => {
update_status( update_status(
context, context,
format!("Building base {}", base_path_rel.display()), format!("Building base {}", base_path_rel.display()),
0, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1;
run_make(&config.build_config, base_path_rel) run_make(&config.build_config, base_path_rel)
} }
_ => BuildStatus::default(), _ => BuildStatus::default(),
@@ -226,44 +247,71 @@ fn run_build(
let time = OffsetDateTime::now_utc(); let time = OffsetDateTime::now_utc();
let first_obj = let first_obj = match &obj_config.target_path {
match &obj_config.target_path { Some(target_path) if first_status.success => {
Some(target_path) if first_status.success => { update_status(
update_status( context,
context, format!("Loading target {}", target_path_rel.unwrap().display()),
format!("Loading target {}", target_path_rel.unwrap().display()), step_idx,
2, total,
total, &cancel,
&cancel, )?;
)?; step_idx += 1;
Some(read::read(target_path, &config.diff_obj_config).with_context(|| { match read::read(target_path, &config.diff_obj_config) {
format!("Failed to read object '{}'", target_path.display()) Ok(obj) => Some(obj),
})?) Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
} }
_ => None, }
}; Some(_) => {
step_idx += 1;
None
}
_ => None,
};
let second_obj = match &obj_config.base_path { let second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => { Some(base_path) if second_status.success => {
update_status( update_status(
context, context,
format!("Loading base {}", base_path_rel.unwrap().display()), format!("Loading base {}", base_path_rel.unwrap().display()),
3, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
Some( step_idx += 1;
read::read(base_path, &config.diff_obj_config) match read::read(base_path, &config.diff_obj_config) {
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?, Ok(obj) => Some(obj),
) Err(e) => {
second_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", base_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
} }
_ => None, _ => None,
}; };
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?; update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?; let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
update_status(context, "Complete".to_string(), total, total, &cancel)?; update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult { Ok(Box::new(ObjDiffResult {
first_status, first_status,
second_status, second_status,
@@ -274,7 +322,7 @@ fn run_build(
} }
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState { pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| { start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
}) })
} }

View File

@@ -58,7 +58,10 @@ fn main() -> ExitCode {
let app_path = std::env::current_exe().ok(); let app_path = std::env::current_exe().ok();
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None)); let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let mut native_options = eframe::NativeOptions::default(); let mut native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
..Default::default()
};
match load_icon() { match load_icon() {
Ok(data) => { Ok(data) => {
native_options.viewport.icon = Some(Arc::new(data)); native_options.viewport.icon = Some(Arc::new(data));

View File

@@ -0,0 +1,82 @@
use egui::{Align, Layout, Sense, Vec2};
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
pub fn render_header(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
mut add_contents: impl FnMut(&mut egui::Ui, usize),
) {
let column_width = available_width / num_columns as f32;
ui.allocate_ui_with_layout(
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
for i in 0..num_columns {
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
add_contents(ui, i);
},
);
}
},
);
ui.separator();
}
pub fn render_table(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
row_height: f32,
total_rows: usize,
mut add_contents: impl FnMut(&mut TableRow, usize),
) {
ui.style_mut().interaction.selectable_labels = false;
let column_width = available_width / num_columns as f32;
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), num_columns)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height)
.sense(Sense::click());
table.body(|body| {
body.rows(row_height, total_rows, |mut row| {
row.set_hovered(false); // Disable hover effect
for i in 0..num_columns {
add_contents(&mut row, i);
}
});
});
}
pub fn render_strips(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
mut add_contents: impl FnMut(&mut egui::Ui, usize),
) {
let column_width = available_width / num_columns as f32;
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|mut strip| {
for i in 0..num_columns {
strip.cell(|ui| {
ui.push_id(i, |ui| {
add_contents(ui, i);
});
});
}
},
);
});
});
}

View File

@@ -2,7 +2,7 @@
use std::string::FromUtf16Error; use std::string::FromUtf16Error;
use std::{ use std::{
mem::take, mem::take,
path::{PathBuf, MAIN_SEPARATOR}, path::{Path, PathBuf, MAIN_SEPARATOR},
}; };
#[cfg(all(windows, feature = "wsl"))] #[cfg(all(windows, feature = "wsl"))]
@@ -19,7 +19,7 @@ use objdiff_core::{
use strum::{EnumMessage, VariantArray}; use strum::{EnumMessage, VariantArray};
use crate::{ use crate::{
app::{AppConfig, AppConfigRef, ObjectConfig}, app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode, config::ProjectObjectNode,
jobs::{ jobs::{
check_update::{start_check_update, CheckUpdateResult}, check_update::{start_check_update, CheckUpdateResult},
@@ -43,7 +43,6 @@ pub struct ConfigViewState {
pub build_running: bool, pub build_running: bool,
pub queue_build: bool, pub queue_build: bool,
pub watch_pattern_text: String, pub watch_pattern_text: String,
pub load_error: Option<String>,
pub object_search: String, pub object_search: String,
pub filter_diffable: bool, pub filter_diffable: bool,
pub filter_incomplete: bool, pub filter_incomplete: bool,
@@ -54,7 +53,7 @@ pub struct ConfigViewState {
} }
impl ConfigViewState { impl ConfigViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) { pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
jobs.results.retain_mut(|result| { jobs.results.retain_mut(|result| {
if let JobResult::CheckUpdate(result) = result { if let JobResult::CheckUpdate(result) = result {
self.check_update = take(result); self.check_update = take(result);
@@ -71,21 +70,21 @@ impl ConfigViewState {
match self.file_dialog_state.poll() { match self.file_dialog_state.poll() {
FileDialogResult::None => {} FileDialogResult::None => {}
FileDialogResult::ProjectDir(path) => { FileDialogResult::ProjectDir(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
guard.set_project_dir(path.to_path_buf()); guard.set_project_dir(path.to_path_buf());
} }
FileDialogResult::TargetDir(path) => { FileDialogResult::TargetDir(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
guard.set_target_obj_dir(path.to_path_buf()); guard.set_target_obj_dir(path.to_path_buf());
} }
FileDialogResult::BaseDir(path) => { FileDialogResult::BaseDir(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
guard.set_base_obj_dir(path.to_path_buf()); guard.set_base_obj_dir(path.to_path_buf());
} }
FileDialogResult::Object(path) => { FileDialogResult::Object(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
if let (Some(base_dir), Some(target_dir)) = if let (Some(base_dir), Some(target_dir)) =
(&guard.base_obj_dir, &guard.target_obj_dir) (&guard.config.base_obj_dir, &guard.config.target_obj_dir)
{ {
if let Ok(obj_path) = path.strip_prefix(base_dir) { if let Ok(obj_path) = path.strip_prefix(base_dir) {
let target_path = target_dir.join(obj_path); let target_path = target_dir.join(obj_path);
@@ -93,9 +92,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(target_path), target_path: Some(target_path),
base_path: Some(path), base_path: Some(path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
}); });
} else if let Ok(obj_path) = path.strip_prefix(target_dir) { } else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
@@ -103,9 +100,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(path), target_path: Some(path),
base_path: Some(base_path), base_path: Some(base_path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
}); });
} }
} }
@@ -113,11 +108,11 @@ impl ConfigViewState {
} }
} }
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) { pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
if self.queue_build { if self.queue_build {
self.queue_build = false; self.queue_build = false;
if let Ok(mut config) = config.write() { if let Ok(mut state) = state.write() {
config.queue_build = true; state.queue_build = true;
} }
} }
@@ -167,42 +162,43 @@ fn fetch_wsl2_distros() -> Vec<String> {
pub fn config_ui( pub fn config_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
config: &AppConfigRef, state: &AppStateRef,
show_config_window: &mut bool, show_config_window: &mut bool,
state: &mut ConfigViewState, config_state: &mut ConfigViewState,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let mut config_guard = config.write().unwrap(); let mut state_guard = state.write().unwrap();
let AppConfig { let AppState {
target_obj_dir, config:
base_obj_dir, AppConfig {
selected_obj, project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
auto_update_check, },
objects, objects,
object_nodes, object_nodes,
.. ..
} = &mut *config_guard; } = &mut *state_guard;
ui.heading("Updates"); ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup"); ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() { if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
state.queue_check_update = true; {
config_state.queue_check_update = true;
} }
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION"))); ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
if let Some(result) = &state.check_update { if let Some(result) = &config_state.check_update {
ui.label(format!("Latest version: {}", result.latest_release.version)); ui.label(format!("Latest version: {}", result.latest_release.version));
if result.update_available { if result.update_available {
ui.colored_label(appearance.insert_color, "Update available"); ui.colored_label(appearance.insert_color, "Update available");
ui.horizontal(|ui| { ui.horizontal(|ui| {
if let Some(bin_name) = &result.found_binary { if let Some(bin_name) = &result.found_binary {
if ui if ui
.add_enabled(!state.update_running, egui::Button::new("Automatic")) .add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
.on_hover_text_at_pointer( .on_hover_text_at_pointer(
"Automatically download and replace the current build", "Automatically download and replace the current build",
) )
.clicked() .clicked()
{ {
state.queue_update = Some(bin_name.clone()); config_state.queue_update = Some(bin_name.clone());
} }
} }
if ui if ui
@@ -227,11 +223,14 @@ pub fn config_ui(
} }
}); });
let mut new_selected_obj = selected_obj.clone(); let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
});
let mut new_selected_index = selected_index;
if objects.is_empty() { if objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() { if ui.button("Select object").clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| { || {
Box::pin( Box::pin(
rfd::AsyncFileDialog::new() rfd::AsyncFileDialog::new()
@@ -254,8 +253,8 @@ pub fn config_ui(
ui.colored_label(appearance.delete_color, "Missing project settings"); ui.colored_label(appearance.delete_color, "Missing project settings");
} }
} else { } else {
let had_search = !state.object_search.is_empty(); let had_search = !config_state.object_search.is_empty();
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui); egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
let mut root_open = None; let mut root_open = None;
let mut node_open = NodeOpen::Default; let mut node_open = NodeOpen::Default;
@@ -277,19 +276,22 @@ pub fn config_ui(
node_open = NodeOpen::Object; node_open = NodeOpen::Object;
} }
let mut filters_text = RichText::new("Filter ⏷"); let mut filters_text = RichText::new("Filter ⏷");
if state.filter_diffable || state.filter_incomplete || state.show_hidden { if config_state.filter_diffable
|| config_state.filter_incomplete
|| config_state.show_hidden
{
filters_text = filters_text.color(appearance.replace_color); filters_text = filters_text.color(appearance.replace_color);
} }
egui::menu::menu_button(ui, filters_text, |ui| { egui::menu::menu_button(ui, filters_text, |ui| {
ui.checkbox(&mut state.filter_diffable, "Diffable") ui.checkbox(&mut config_state.filter_diffable, "Diffable")
.on_hover_text_at_pointer("Only show objects with a source file"); .on_hover_text_at_pointer("Only show objects with a source file");
ui.checkbox(&mut state.filter_incomplete, "Incomplete") ui.checkbox(&mut config_state.filter_incomplete, "Incomplete")
.on_hover_text_at_pointer("Only show objects not marked complete"); .on_hover_text_at_pointer("Only show objects not marked complete");
ui.checkbox(&mut state.show_hidden, "Hidden") ui.checkbox(&mut config_state.show_hidden, "Hidden")
.on_hover_text_at_pointer("Show hidden (auto-generated) objects"); .on_hover_text_at_pointer("Show hidden (auto-generated) objects");
}); });
}); });
if state.object_search.is_empty() { if config_state.object_search.is_empty() {
if had_search { if had_search {
root_open = Some(true); root_open = Some(true);
node_open = NodeOpen::Object; node_open = NodeOpen::Object;
@@ -306,45 +308,55 @@ pub fn config_ui(
.open(root_open) .open(root_open)
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
let search = state.object_search.to_ascii_lowercase(); let search = config_state.object_search.to_ascii_lowercase();
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
for node in object_nodes.iter().filter_map(|node| { for node in object_nodes.iter().filter_map(|node| {
filter_node( filter_node(
objects,
node, node,
&search, &search,
state.filter_diffable, config_state.filter_diffable,
state.filter_incomplete, config_state.filter_incomplete,
state.show_hidden, config_state.show_hidden,
) )
}) { }) {
display_node(ui, &mut new_selected_obj, &node, appearance, node_open); display_node(
ui,
&mut new_selected_index,
project_dir.as_deref(),
objects,
&node,
appearance,
node_open,
);
} }
}); });
} }
if new_selected_obj != *selected_obj { if new_selected_index != selected_index {
if let Some(obj) = new_selected_obj { if let Some(idx) = new_selected_index {
// Will set obj_changed, which will trigger a rebuild // Will set obj_changed, which will trigger a rebuild
config_guard.set_selected_obj(obj); let config = ObjectConfig::from(&objects[idx]);
state_guard.set_selected_obj(config);
} }
} }
if config_guard.selected_obj.is_some() if state_guard.config.selected_obj.is_some()
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() && ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
{ {
state.queue_build = true; config_state.queue_build = true;
} }
ui.separator();
} }
fn display_object( fn display_unit(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>,
name: &str, name: &str,
object: &ProjectObject, units: &[ProjectObject],
index: usize,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let object_name = object.name(); let object = &units[index];
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let selected = *selected_obj == Some(index);
let color = if selected { let color = if selected {
appearance.emphasized_text_color appearance.emphasized_text_color
} else if let Some(complete) = object.complete() { } else if let Some(complete) = object.complete() {
@@ -356,7 +368,7 @@ fn display_object(
} else { } else {
appearance.text_color appearance.text_color
}; };
let clicked = SelectableLabel::new( let response = SelectableLabel::new(
selected, selected,
RichText::new(name) RichText::new(name)
.font(FontId { .font(FontId {
@@ -365,19 +377,32 @@ fn display_object(
}) })
.color(color), .color(color),
) )
.ui(ui) .ui(ui);
.clicked(); if get_source_path(project_dir, object).is_some() {
// Always recreate ObjectConfig if selected, in case the project config changed. response.context_menu(|ui| object_context_ui(ui, object, project_dir));
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. }
if selected || clicked { if response.clicked() {
*selected_obj = Some(ObjectConfig { *selected_obj = Some(index);
name: object_name.to_string(), }
target_path: object.target_path.clone(), }
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order(), fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
complete: object.complete(), project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
scratch: object.scratch.clone(), }
});
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
if let Some(source_path) = get_source_path(project_dir, object) {
if ui
.button("Open source file")
.on_hover_text("Open the source file in the default editor")
.clicked()
{
log::info!("Opening file {}", source_path.display());
if let Err(e) = open::that_detached(&source_path) {
log::error!("Failed to open source file: {e}");
}
ui.close_menu();
}
} }
} }
@@ -392,17 +417,19 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>,
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
display_object(ui, selected_obj, name, object, appearance); display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
let open = match node_open { let open = match node_open {
NodeOpen::Default => None, NodeOpen::Default => None,
NodeOpen::Open => Some(true), NodeOpen::Open => Some(true),
@@ -425,16 +452,16 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { for node in children {
display_node(ui, selected_obj, node, appearance, node_open); display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
} }
}); });
} }
} }
} }
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool { fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
match node { match node {
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name, ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
ProjectObjectNode::Dir(_, children) => { ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, selected_obj)) children.iter().any(|node| contains_node(node, selected_obj))
} }
@@ -442,6 +469,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
} }
fn filter_node( fn filter_node(
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
search: &str, search: &str,
filter_diffable: bool, filter_diffable: bool,
@@ -449,12 +477,12 @@ fn filter_node(
show_hidden: bool, show_hidden: bool,
) -> Option<ProjectObjectNode> { ) -> Option<ProjectObjectNode> {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
let unit = &units[*idx];
if (search.is_empty() || name.to_ascii_lowercase().contains(search)) if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable && (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|| (object.base_path.is_some() && object.target_path.is_some())) && (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
&& (!filter_incomplete || matches!(object.complete(), None | Some(false))) && (show_hidden || !unit.hidden())
&& (show_hidden || !object.hidden())
{ {
Some(node.clone()) Some(node.clone())
} else { } else {
@@ -465,7 +493,14 @@ fn filter_node(
let new_children = children let new_children = children
.iter() .iter()
.filter_map(|child| { .filter_map(|child| {
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden) filter_node(
units,
child,
search,
filter_diffable,
filter_incomplete,
show_hidden,
)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !new_children.is_empty() { if !new_children.is_empty() {
@@ -523,33 +558,33 @@ fn pick_folder_ui(
pub fn project_window( pub fn project_window(
ctx: &egui::Context, ctx: &egui::Context,
config: &AppConfigRef, state: &AppStateRef,
show: &mut bool, show: &mut bool,
state: &mut ConfigViewState, config_state: &mut ConfigViewState,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let mut config_guard = config.write().unwrap(); let mut state_guard = state.write().unwrap();
egui::Window::new("Project").open(show).show(ctx, |ui| { egui::Window::new("Project").open(show).show(ctx, |ui| {
split_obj_config_ui(ui, &mut config_guard, state, appearance); split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
}); });
if let Some(error) = &state.load_error { if let Some(error) = &state_guard.config_error {
let mut open = true; let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| { egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:"); ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error); ui.colored_label(appearance.delete_color, error);
}); });
if !open { if !open {
state.load_error = None; state_guard.config_error = None;
} }
} }
} }
fn split_obj_config_ui( fn split_obj_config_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
config: &mut AppConfig, state: &mut AppState,
state: &mut ConfigViewState, config_state: &mut ConfigViewState,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color); let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
@@ -560,7 +595,7 @@ fn split_obj_config_ui(
let response = pick_folder_ui( let response = pick_folder_ui(
ui, ui,
&config.project_dir, &state.config.project_dir,
"Project directory", "Project directory",
|ui| { |ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -576,7 +611,7 @@ fn split_obj_config_ui(
true, true,
); );
if response.clicked() { if response.clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()), || Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
FileDialogResult::ProjectDir, FileDialogResult::ProjectDir,
); );
@@ -605,33 +640,35 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}); });
}); });
let mut custom_make_str = config.custom_make.clone().unwrap_or_default(); let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
if ui if ui
.add_enabled( .add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"), egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.changed() .changed()
{ {
if custom_make_str.is_empty() { if custom_make_str.is_empty() {
config.custom_make = None; state.config.custom_make = None;
} else { } else {
config.custom_make = Some(custom_make_str); state.config.custom_make = Some(custom_make_str);
} }
} }
#[cfg(all(windows, feature = "wsl"))] #[cfg(all(windows, feature = "wsl"))]
{ {
if state.available_wsl_distros.is_none() { if config_state.available_wsl_distros.is_none() {
state.available_wsl_distros = Some(fetch_wsl2_distros()); config_state.available_wsl_distros = Some(fetch_wsl2_distros());
} }
egui::ComboBox::from_label("Run in WSL2") egui::ComboBox::from_label("Run in WSL2")
.selected_text(config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string())) .selected_text(
state.config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()),
)
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled"); ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
for distro in state.available_wsl_distros.as_ref().unwrap() { for distro in config_state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value( ui.selectable_value(
&mut config.selected_wsl_distro, &mut state.config.selected_wsl_distro,
Some(distro.clone()), Some(distro.clone()),
distro, distro,
); );
@@ -640,10 +677,10 @@ fn split_obj_config_ui(
} }
ui.separator(); ui.separator();
if let Some(project_dir) = config.project_dir.clone() { if let Some(project_dir) = state.config.project_dir.clone() {
let response = pick_folder_ui( let response = pick_folder_ui(
ui, ui,
&config.target_obj_dir, &state.config.target_obj_dir,
"Target build directory", "Target build directory",
|ui| { |ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -660,17 +697,17 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
config.project_config_info.is_none(), state.project_config_info.is_none(),
); );
if response.clicked() { if response.clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()), || Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
FileDialogResult::TargetDir, FileDialogResult::TargetDir,
); );
} }
ui.add_enabled( ui.add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_target, "Build target objects"), egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| { .on_hover_ui(|ui| {
@@ -704,7 +741,7 @@ fn split_obj_config_ui(
let response = pick_folder_ui( let response = pick_folder_ui(
ui, ui,
&config.base_obj_dir, &state.config.base_obj_dir,
"Base build directory", "Base build directory",
|ui| { |ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -716,17 +753,17 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
config.project_config_info.is_none(), state.project_config_info.is_none(),
); );
if response.clicked() { if response.clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()), || Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
FileDialogResult::BaseDir, FileDialogResult::BaseDir,
); );
} }
ui.add_enabled( ui.add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_base, "Build base objects"), egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| { .on_hover_ui(|ui| {
@@ -757,7 +794,7 @@ fn split_obj_config_ui(
subheading(ui, "Watch settings", appearance); subheading(ui, "Watch settings", appearance);
let response = let response =
ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| { ui.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
job.append( job.append(
"Automatically re-run the build & diff when files change.", "Automatically re-run the build & diff when files change.",
@@ -767,23 +804,23 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}); });
if response.changed() { if response.changed() {
config.watcher_change = true; state.watcher_change = true;
}; };
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color)); ui.label(RichText::new("File patterns").color(appearance.text_color));
if ui if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("Reset")) .add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset"))
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
config.watch_patterns = state.config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
config.watcher_change = true; state.watcher_change = true;
} }
}); });
let mut remove_at: Option<usize> = None; let mut remove_at: Option<usize> = None;
for (idx, glob) in config.watch_patterns.iter().enumerate() { for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new(format!("{}", glob)) RichText::new(format!("{}", glob))
@@ -791,7 +828,7 @@ fn split_obj_config_ui(
.family(FontFamily::Monospace), .family(FontFamily::Monospace),
); );
if ui if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("-").small()) .add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
@@ -800,24 +837,24 @@ fn split_obj_config_ui(
}); });
} }
if let Some(idx) = remove_at { if let Some(idx) = remove_at {
config.watch_patterns.remove(idx); state.config.watch_patterns.remove(idx);
config.watcher_change = true; state.watcher_change = true;
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_enabled( ui.add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0), egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT); .on_disabled_hover_text(CONFIG_DISABLED_TEXT);
if ui if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("+").small()) .add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
if let Ok(glob) = Glob::new(&state.watch_pattern_text) { if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
config.watch_patterns.push(glob); state.config.watch_patterns.push(glob);
config.watcher_change = true; state.watcher_change = true;
state.watch_pattern_text.clear(); config_state.watch_pattern_text.clear();
} }
} }
}); });
@@ -825,131 +862,131 @@ fn split_obj_config_ui(
pub fn arch_config_window( pub fn arch_config_window(
ctx: &egui::Context, ctx: &egui::Context,
config: &AppConfigRef, state: &AppStateRef,
show: &mut bool, show: &mut bool,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let mut config_guard = config.write().unwrap(); let mut state_guard = state.write().unwrap();
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| { egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
arch_config_ui(ui, &mut config_guard, appearance); arch_config_ui(ui, &mut state_guard, appearance);
}); });
} }
fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) { fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
ui.heading("x86"); ui.heading("x86");
egui::ComboBox::new("x86_formatter", "Format") egui::ComboBox::new("x86_formatter", "Format")
.selected_text(config.diff_obj_config.x86_formatter.get_message().unwrap()) .selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for &formatter in X86Formatter::VARIANTS { for &formatter in X86Formatter::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.x86_formatter == formatter, state.config.diff_obj_config.x86_formatter == formatter,
formatter.get_message().unwrap(), formatter.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.x86_formatter = formatter; state.config.diff_obj_config.x86_formatter = formatter;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
ui.separator(); ui.separator();
ui.heading("MIPS"); ui.heading("MIPS");
egui::ComboBox::new("mips_abi", "ABI") egui::ComboBox::new("mips_abi", "ABI")
.selected_text(config.diff_obj_config.mips_abi.get_message().unwrap()) .selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for &abi in MipsAbi::VARIANTS { for &abi in MipsAbi::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.mips_abi == abi, state.config.diff_obj_config.mips_abi == abi,
abi.get_message().unwrap(), abi.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.mips_abi = abi; state.config.diff_obj_config.mips_abi = abi;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
egui::ComboBox::new("mips_instr_category", "Instruction Category") egui::ComboBox::new("mips_instr_category", "Instruction Category")
.selected_text(config.diff_obj_config.mips_instr_category.get_message().unwrap()) .selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for &category in MipsInstrCategory::VARIANTS { for &category in MipsInstrCategory::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.mips_instr_category == category, state.config.diff_obj_config.mips_instr_category == category,
category.get_message().unwrap(), category.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.mips_instr_category = category; state.config.diff_obj_config.mips_instr_category = category;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
ui.separator(); ui.separator();
ui.heading("ARM"); ui.heading("ARM");
egui::ComboBox::new("arm_arch_version", "Architecture Version") egui::ComboBox::new("arm_arch_version", "Architecture Version")
.selected_text(config.diff_obj_config.arm_arch_version.get_message().unwrap()) .selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for &version in ArmArchVersion::VARIANTS { for &version in ArmArchVersion::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.arm_arch_version == version, state.config.diff_obj_config.arm_arch_version == version,
version.get_message().unwrap(), version.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.arm_arch_version = version; state.config.diff_obj_config.arm_arch_version = version;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
let response = ui let response = ui
.checkbox(&mut config.diff_obj_config.arm_unified_syntax, "Unified syntax") .checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax")
.on_hover_text("Disassemble as unified assembly language (UAL)."); .on_hover_text("Disassemble as unified assembly language (UAL).");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
let response = ui let response = ui
.checkbox(&mut config.diff_obj_config.arm_av_registers, "Use A/V registers") .checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8"); .on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
egui::ComboBox::new("arm_r9_usage", "Display R9 as") egui::ComboBox::new("arm_r9_usage", "Display R9 as")
.selected_text(config.diff_obj_config.arm_r9_usage.get_message().unwrap()) .selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for &usage in ArmR9Usage::VARIANTS { for &usage in ArmR9Usage::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.arm_r9_usage == usage, state.config.diff_obj_config.arm_r9_usage == usage,
usage.get_message().unwrap(), usage.get_message().unwrap(),
) )
.on_hover_text(usage.get_detailed_message().unwrap()) .on_hover_text(usage.get_detailed_message().unwrap())
.clicked() .clicked()
{ {
config.diff_obj_config.arm_r9_usage = usage; state.config.diff_obj_config.arm_r9_usage = usage;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
let response = ui let response = ui
.checkbox(&mut config.diff_obj_config.arm_sl_usage, "Display R10 as SL") .checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
.on_hover_text("Used for explicit stack limits."); .on_hover_text("Used for explicit stack limits.");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
let response = ui let response = ui
.checkbox(&mut config.diff_obj_config.arm_fp_usage, "Display R11 as FP") .checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
.on_hover_text("Used for frame pointers."); .on_hover_text("Used for frame pointers.");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
let response = ui let response = ui
.checkbox(&mut config.diff_obj_config.arm_ip_usage, "Display R12 as IP") .checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
.on_hover_text("Used for interworking and long branches."); .on_hover_text("Used for interworking and long branches.");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
} }

View File

@@ -1,7 +1,6 @@
use std::{cmp::min, default::Default, mem::take}; use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder};
use objdiff_core::{ use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
obj::ObjInfo, obj::ObjInfo,
@@ -10,14 +9,15 @@ use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text, write_text,
}; };
const BYTES_PER_ROW: usize = 16; const BYTES_PER_ROW: usize = 16;
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> { fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == selected_symbol.section_name) obj.sections.iter().position(|section| section.name == section_name)
} }
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) { fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
@@ -131,20 +131,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
split_diffs split_diffs
} }
#[derive(Clone, Copy)]
struct SectionDiffContext<'a> {
obj: &'a ObjInfo,
diff: &'a ObjDiff,
section_index: Option<usize>,
}
impl<'a> SectionDiffContext<'a> {
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
obj.map(|(obj, diff)| Self {
obj,
diff,
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
})
}
#[inline]
pub fn has_section(&self) -> bool { self.section_index.is_some() }
}
fn data_table_ui( fn data_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<SectionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<SectionDiffContext<'_>>,
config: &Appearance, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_section = left_obj.and_then(|(obj, diff)| { let left_section = left_ctx
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
}); let right_section = right_ctx
let right_section = right_obj.and_then(|(obj, diff)| { .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
});
let total_bytes = left_section let total_bytes = left_section
.or(right_section)? .or(right_section)?
.1 .1
@@ -159,118 +176,117 @@ fn data_table_ui(
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff)); let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff)); let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
table.body(|body| { render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
body.rows(config.code_font.size, total_rows, |mut row| { let i = row.index();
let row_index = row.index(); let address = i * BYTES_PER_ROW;
let address = row_index * BYTES_PER_ROW; row.col(|ui| {
row.col(|ui| { if column == 0 {
if let Some(left_diffs) = &left_diffs { if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[row_index], config); data_row_ui(ui, address, &left_diffs[i], config);
} }
}); } else if column == 1 {
row.col(|ui| {
if let Some(right_diffs) = &right_diffs { if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[row_index], config); data_row_ui(ui, address, &right_diffs[i], config);
} }
}); }
}); });
}); });
Some(()) Some(())
} }
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn data_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let section_name =
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
);
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
// If both sides are missing a symbol, switch to symbol diff view
if !right_ctx.map_or(false, |ctx| ctx.has_section())
&& !left_ctx.map_or(false, |ctx| ctx.has_section())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( if ui.button("⏴ Back").clicked() {
Vec2 { x: column_width, y: 100.0 }, ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
Layout::top_down(Align::Min), }
|ui| {
ui.set_width(column_width);
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
);
if let Some(section) =
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
{
ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
});
ui.horizontal(|ui| { if let Some(section) =
if ui right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
.add_enabled(!state.build_running, egui::Button::new("Build")) {
.clicked() ui.label(
{ RichText::new(section.name.clone())
state.queue_build = true; .font(appearance.code_font.clone())
} .color(appearance.highlight_color),
ui.scope(|ui| { );
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); } else {
if state.build_running { ui.label(
ui.colored_label(appearance.replace_color, "Building"); RichText::new("Missing")
} else { .font(appearance.code_font.clone())
ui.label("Last built:"); .color(appearance.replace_color),
let format = );
format_description::parse("[hour]:[minute]:[second]").unwrap(); }
ui.label( }
result });
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("");
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table // Table
ui.style_mut().interaction.selectable_labels = false; let id =
let available_height = ui.available_height(); Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
let table = TableBuilder::new(ui) .with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
.striped(false) ui.push_id(id, |ui| {
.cell_layout(Layout::left_to_right(Align::Min)) data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
.columns(Column::exact(column_width).clip(true), 2) });
.resizable(false) ret
.auto_shrink([false, false])
.min_scrolled_height(available_height);
data_table_ui(
table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
);
} }

View File

@@ -1,42 +1,34 @@
use egui::{Align, Layout, ScrollArea, Ui, Vec2}; use egui::{RichText, ScrollArea};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{ use objdiff_core::{
arch::ppc::ExceptionInfo, arch::ppc::ExceptionInfo,
diff::ObjDiff, obj::{ObjInfo, ObjSymbol},
obj::{ObjInfo, ObjSymbol, SymbolRef},
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
View,
},
}; };
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == selected_symbol.symbol_name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn decode_extab(extab: &ExceptionInfo) -> String { fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from(""); let mut text = String::from("");
let mut dtor_names: Vec<&str> = vec![]; let mut dtor_names: Vec<String> = vec![];
for dtor in &extab.dtors { for dtor in &extab.dtors {
//For each function name, use the demangled name by default, //For each function name, use the demangled name by default,
//and if not available fallback to the original name //and if not available fallback to the original name
let name = match &dtor.demangled_name { let name: String = match &dtor.demangled_name {
Some(demangled_name) => demangled_name, Some(demangled_name) => demangled_name.to_string(),
None => &dtor.name, None => dtor.name.clone(),
}; };
dtor_names.push(name.as_str()); dtor_names.push(name);
} }
if let Some(decoded) = extab.data.to_string(&dtor_names) { if let Some(decoded) = extab.data.to_string(dtor_names) {
text += decoded.as_str(); text += decoded.as_str();
} }
@@ -48,14 +40,12 @@ fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a Exce
} }
fn extab_text_ui( fn extab_text_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
) -> Option<()> { ) -> Option<()> {
let (_section, symbol) = obj.0.section_symbol(symbol_ref); if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
let text = decode_extab(extab_entry); let text = decode_extab(extab_entry);
ui.colored_label(appearance.replace_color, &text); ui.colored_label(appearance.replace_color, &text);
return Some(()); return Some(());
@@ -65,137 +55,194 @@ fn extab_text_ui(
} }
fn extab_ui( fn extab_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: Option<&(ObjInfo, ObjDiff)>, ctx: FunctionDiffContext<'_>,
selected_symbol: &SymbolRefByName,
appearance: &Appearance, appearance: &Appearance,
_left: bool, _column: usize,
) { ) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); if let Some((_section, symbol)) =
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
if let (Some(object), Some(symbol_ref)) = (obj, symbol) { {
extab_text_ui(ui, object, symbol_ref, appearance); extab_text_ui(ui, ctx, symbol, appearance);
} }
}); });
}); });
} }
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn extab_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(symbol_ref),
right_symbol: state.symbol_state.right_symbol.clone(),
}));
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.button("⏴ Back").clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|ui| { }
ui.set_width(column_width); ui.separator();
if ui
ui.horizontal(|ui| { .add_enabled(
if ui.button("⏴ Back").clicked() { !state.scratch_running
state.current_view = View::SymbolDiff; && state.scratch_available
} && left_ctx.map_or(false, |ctx| ctx.has_symbol()),
}); egui::Button::new("📲 decomp.me"),
)
let name = selected_symbol .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.demangled_symbol_name .on_disabled_hover_text("Scratch configuration missing")
.as_deref() .clicked()
.unwrap_or(&selected_symbol.symbol_name); {
ui.scope(|ui| { if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
ui.colored_label(appearance.highlight_color, name); }) {
ui.label("Diff target:"); ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}); }
}, }
); });
if let Some((_section, symbol)) = left_ctx
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
{
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
ui.horizontal(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
if ui ctx.symbol_ref.map(|symbol_ref| {
.add_enabled(!state.build_running, egui::Button::new("Build")) (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.clicked() })
{ }) {
state.queue_build = true; let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
} ui.label(
ui.scope(|ui| { RichText::new(name)
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); .font(appearance.code_font.clone())
if state.build_running { .color(appearance.highlight_color),
ui.colored_label(appearance.replace_color, "Building…"); );
} else { if let Some(match_percent) = symbol_diff.match_percent {
ui.label("Last built:"); ui.label(
let format = RichText::new(format!("{:.0}%", match_percent.floor()))
format_description::parse("[hour]:[minute]:[second]").unwrap(); .font(appearance.code_font.clone())
ui.label( .color(match_color_for_symbol(match_percent, appearance)),
result );
.time }
.to_offset(appearance.utc_offset) } else {
.format(&format) ui.label(
.unwrap(), RichText::new("Missing")
); .font(appearance.code_font.clone())
} .color(appearance.replace_color),
}); );
}); }
}
ui.scope(|ui| { });
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|(obj, diff)| {
find_symbol(obj, selected_symbol).map(|sref| {
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
})
})
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table // Table
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { render_strips(ui, available_width, 2, |ui, column| {
strip.strip(|builder| { if column == 0 {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { if let Some(ctx) = left_ctx {
strip.cell(|ui| { extab_ui(ui, ctx, appearance, column);
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true); }
}); } else if column == 1 {
strip.cell(|ui| { if let Some(ctx) = right_ctx {
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false); extab_ui(ui, ctx, appearance, column);
}); }
}); }
});
}); });
ret
} }

View File

@@ -1,28 +1,29 @@
use std::default::Default; use std::default::Default;
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder, TableRow}; use egui_extras::TableRow;
use objdiff_core::{ use objdiff_core::{
arch::ObjArch, arch::ObjArch,
diff::{ diff::{
display::{display_diff, DiffText, HighlightKind}, display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiff, ObjInsDiffKind, ObjDiff, ObjInsDiff, ObjInsDiffKind,
}, },
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef}, obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
SymbolRef,
},
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_strips, render_table},
symbol_diff::{
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
},
}; };
#[derive(Copy, Clone, Eq, PartialEq)]
enum ColumnId {
Left,
Right,
}
#[derive(Default)] #[derive(Default)]
pub struct FunctionViewState { pub struct FunctionViewState {
left_highlight: HighlightKind, left_highlight: HighlightKind,
@@ -30,16 +31,17 @@ pub struct FunctionViewState {
} }
impl FunctionViewState { impl FunctionViewState {
fn highlight(&self, column: ColumnId) -> &HighlightKind { pub fn highlight(&self, column: usize) -> &HighlightKind {
match column { match column {
ColumnId::Left => &self.left_highlight, 0 => &self.left_highlight,
ColumnId::Right => &self.right_highlight, 1 => &self.right_highlight,
_ => &HighlightKind::None,
} }
} }
fn set_highlight(&mut self, column: ColumnId, highlight: HighlightKind) { pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
match column { match column {
ColumnId::Left => { 0 => {
if highlight == self.left_highlight { if highlight == self.left_highlight {
if highlight == self.right_highlight { if highlight == self.right_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@@ -51,7 +53,7 @@ impl FunctionViewState {
self.left_highlight = highlight; self.left_highlight = highlight;
} }
} }
ColumnId::Right => { 1 => {
if highlight == self.right_highlight { if highlight == self.right_highlight {
if highlight == self.left_highlight { if highlight == self.left_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@@ -63,8 +65,14 @@ impl FunctionViewState {
self.right_highlight = highlight; self.right_highlight = highlight;
} }
} }
_ => {}
} }
} }
pub fn clear_highlight(&mut self) {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} }
fn ins_hover_ui( fn ins_hover_ui(
@@ -218,17 +226,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None None
} }
#[allow(clippy::too_many_arguments)] #[must_use]
#[expect(clippy::too_many_arguments)]
fn diff_text_ui( fn diff_text_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
text: DiffText<'_>, text: DiffText<'_>,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
space_width: f32, space_width: f32,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
let label_text; let label_text;
let mut base_color = match ins_diff.kind { let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
@@ -282,7 +292,7 @@ fn diff_text_ui(
} }
DiffText::Spacing(n) => { DiffText::Spacing(n) => {
ui.add_space(n as f32 * space_width); ui.add_space(n as f32 * space_width);
return; return ret;
} }
DiffText::Eol => { DiffText::Eol => {
label_text = "\n".to_string(); label_text = "\n".to_string();
@@ -299,22 +309,25 @@ fn diff_text_ui(
.ui(ui); .ui(ui);
response = response_cb(response); response = response_cb(response);
if response.clicked() { if response.clicked() {
ins_view_state.set_highlight(column, text.into()); ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
} }
if len < pad_to { if len < pad_to {
ui.add_space((pad_to - len) as f32 * space_width); ui.add_space((pad_to - len) as f32 * space_width);
} }
ret
} }
#[must_use]
fn asm_row_ui( fn asm_row_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
symbol: &ObjSymbol, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ins_diff.kind != ObjInsDiffKind::None { if ins_diff.kind != ObjInsDiffKind::None {
@@ -322,7 +335,7 @@ fn asm_row_ui(
} }
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| { display_diff(ins_diff, symbol.address, |text| {
diff_text_ui( if let Some(action) = diff_text_ui(
ui, ui,
text, text,
ins_diff, ins_diff,
@@ -331,229 +344,476 @@ fn asm_row_ui(
column, column,
space_width, space_width,
&response_cb, &response_cb,
); ) {
ret = Some(action);
}
Ok::<_, ()>(()) Ok::<_, ()>(())
}) })
.unwrap(); .unwrap();
ret
} }
#[must_use]
fn asm_col_ui( fn asm_col_ui(
row: &mut TableRow<'_, '_>, row: &mut TableRow<'_, '_>,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
) { ) -> Option<DiffViewAction> {
let (section, symbol) = obj.0.section_symbol(symbol_ref); let mut ret = None;
let section = section.unwrap(); let symbol_ref = ctx.symbol_ref?;
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()]; let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
let section = section?;
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
let response_cb = |response: Response| { let response_cb = |response: Response| {
if let Some(ins) = &ins_diff.ins { if let Some(ins) = &ins_diff.ins {
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol)); response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
response.on_hover_ui_at_pointer(|ui| { response.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance) ins_hover_ui(ui, ctx.obj.arch.as_ref(), section, ins, symbol, appearance)
}) })
} else { } else {
response response
} }
}; };
let (_, response) = row.col(|ui| { let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb); if let Some(action) =
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
{
ret = Some(action);
}
}); });
response_cb(response); response_cb(response);
ret
} }
fn empty_col_ui(row: &mut TableRow<'_, '_>) { #[must_use]
row.col(|ui| {
ui.label("");
});
}
fn asm_table_ui( fn asm_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<FunctionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<FunctionDiffContext<'_>>,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
) -> Option<()> { symbol_state: &SymbolViewState,
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); ) -> Option<DiffViewAction> {
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); let mut ret = None;
let instructions_len = match (left_symbol, right_symbol) { let left_len = left_ctx.and_then(|ctx| {
(Some(left_symbol_ref), Some(right_symbol_ref)) => { ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len(); });
let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len(); let right_len = right_ctx.and_then(|ctx| {
debug_assert_eq!(left_len, right_len); ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
});
let instructions_len = match (left_len, right_len) {
(Some(left_len), Some(right_len)) => {
if left_len != right_len {
ui.label("Instruction count mismatch");
return None;
}
left_len left_len
} }
(Some(left_symbol_ref), None) => { (Some(left_len), None) => left_len,
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len() (None, Some(right_len)) => right_len,
(None, None) => {
ui.label("No symbol selected");
return None;
} }
(None, Some(right_symbol_ref)) => {
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
}
(None, None) => return None,
}; };
table.body(|body| { if left_len.is_some() && right_len.is_some() {
body.rows(appearance.code_font.size, instructions_len, |mut row| { // Joint view
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) { render_table(
asm_col_ui( ui,
&mut row, available_width,
left_obj, 2,
left_symbol_ref, appearance.code_font.size,
appearance, instructions_len,
ins_view_state, |row, column| {
ColumnId::Left, if column == 0 {
); if let Some(ctx) = left_ctx {
} else { if let Some(action) =
empty_col_ui(&mut row); asm_col_ui(row, ctx, appearance, ins_view_state, column)
} {
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) { ret = Some(action);
asm_col_ui( }
&mut row, }
right_obj, } else if column == 1 {
right_symbol_ref, if let Some(ctx) = right_ctx {
appearance, if let Some(action) =
ins_view_state, asm_col_ui(row, ctx, appearance, ins_view_state, column)
ColumnId::Right, {
); ret = Some(action);
} else { }
empty_col_ui(&mut row); }
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
}
},
);
} else {
// Split view, one side is the symbol list
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {
if let Some(ctx) = left_ctx {
if ctx.has_symbol() {
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((right_ctx, right_symbol_ref)) =
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(right_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
left_symbol: Some(left_symbol_ref),
..
}) => {
let (right_section, right_symbol) =
right_ctx.obj.section_symbol(right_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match right_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
left_symbol_ref,
SymbolRefByName::new(right_symbol, right_section),
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
}
}
}
} else {
ui.label("No left object");
}
} else if column == 1 {
if let Some(ctx) = right_ctx {
if ctx.has_symbol() {
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((left_ctx, left_symbol_ref)) =
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(left_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
right_symbol: Some(right_symbol_ref),
..
}) => {
let (left_section, left_symbol) =
left_ctx.obj.section_symbol(left_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match left_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
SymbolRefByName::new(left_symbol, left_section),
right_symbol_ref,
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
}
}
}
} else {
ui.label("No right object");
}
} }
}); });
}); }
Some(()) ret
} }
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[derive(Clone, Copy)]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub struct FunctionDiffContext<'a> {
else { pub obj: &'a ObjInfo,
return; pub diff: &'a ObjDiff,
pub symbol_ref: Option<SymbolRef>,
}
impl<'a> FunctionDiffContext<'a> {
pub fn new(
obj: Option<&'a (ObjInfo, ObjDiff)>,
selected_symbol: Option<&SymbolRefByName>,
) -> Option<Self> {
obj.map(|(obj, diff)| Self {
obj,
diff,
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
})
}
#[inline]
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
}
#[must_use]
pub fn function_diff_ui(
ui: &mut egui::Ui,
state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(symbol_ref),
right_symbol: state.symbol_state.right_symbol.clone(),
}));
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.button("⏴ Back").clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|ui| { }
ui.set_width(column_width); ui.separator();
if ui
.add_enabled(
!state.scratch_running
&& state.scratch_available
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
egui::Button::new("📲 decomp.me"),
)
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing")
.clicked()
{
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
}) {
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}
}
});
ui.horizontal(|ui| { if let Some((_section, symbol)) = left_ctx
if ui.button("⏴ Back").clicked() { .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
state.current_view = View::SymbolDiff; {
} let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
if right_ctx.map_or(false, |m| m.has_symbol())
&& ui
.button("Change target")
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
.clicked()
{
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
}
}
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.label(
RichText::new("Choose target symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| {
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
})
}) {
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
ui.horizontal(|ui| {
if let Some(match_percent) = symbol_diff.match_percent {
ui.label(
RichText::new(format!("{:.0}%", match_percent.floor()))
.font(appearance.code_font.clone())
.color(match_color_for_symbol(match_percent, appearance)),
);
}
if left_ctx.map_or(false, |m| m.has_symbol()) {
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .button("Change base")
!state.scratch_running && state.scratch_available, .on_hover_text_at_pointer(
egui::Button::new("📲 decomp.me"), "Choose a different symbol to use as the base",
) )
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing")
.clicked() .clicked()
{ {
state.queue_scratch = true; if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
} ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
});
let name = selected_symbol
.demangled_symbol_name
.as_deref()
.unwrap_or(&selected_symbol.symbol_name);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, name);
ui.label("Diff target:");
});
},
);
// Right column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
} }
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|(obj, diff)| {
find_symbol(obj, selected_symbol).map(|sref| {
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
})
})
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
} }
ui.label("Diff base:"); }
}); });
}, } else {
); ui.label(
}, RichText::new("Missing")
); .font(appearance.code_font.clone())
ui.separator(); .color(appearance.replace_color),
);
ui.label(
RichText::new("Choose base symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
}
});
// Table // Table
ui.style_mut().interaction.selectable_labels = false; let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
let available_height = ui.available_height(); .with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
let table = TableBuilder::new(ui) if let Some(action) = ui
.striped(false) .push_id(id, |ui| {
.cell_layout(Layout::left_to_right(Align::Min)) asm_table_ui(
.columns(Column::exact(column_width).clip(true), 2) ui,
.resizable(false) available_width,
.auto_shrink([false, false]) left_ctx,
.min_scrolled_height(available_height); right_ctx,
asm_table_ui( appearance,
table, &state.function_state,
result.first_obj.as_ref(), &state.symbol_state,
result.second_obj.as_ref(), )
selected_symbol, })
appearance, .inner
&mut state.function_state, {
); ret = Some(action);
}
ret
} }

View File

@@ -1,5 +1,6 @@
use std::{ use std::{
fs::File, fs::File,
io::{BufReader, BufWriter},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -46,13 +47,13 @@ pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
if !path.exists() { if !path.exists() {
return Ok(None); return Ok(None);
} }
let file = File::open(path)?; let file = BufReader::new(File::open(path)?);
let config: GraphicsConfig = ron::de::from_reader(file)?; let config: GraphicsConfig = ron::de::from_reader(file)?;
Ok(Some(config)) Ok(Some(config))
} }
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> { pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
let file = File::create(path)?; let file = BufWriter::new(File::create(path)?);
ron::ser::to_writer(file, config)?; ron::ser::to_writer(file, config)?;
Ok(()) Ok(())
} }

View File

@@ -1,58 +1,162 @@
use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget}; use egui::{ProgressBar, RichText, Widget};
use crate::{jobs::JobQueue, views::appearance::Appearance}; use crate::{
jobs::{JobQueue, JobStatus},
views::appearance::Appearance,
};
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) { pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
ui.label("Jobs"); if ui.button("Clear").clicked() {
jobs.clear_errored();
}
let mut remove_job: Option<usize> = None; let mut remove_job: Option<usize> = None;
let mut any_jobs = false;
for job in jobs.iter_mut() { for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else { let Ok(status) = job.context.status.read() else {
continue; continue;
}; };
ui.group(|ui| { any_jobs = true;
ui.horizontal(|ui| { ui.separator();
ui.label(&status.title); ui.horizontal(|ui| {
if ui.small_button("").clicked() { ui.label(&status.title);
if job.handle.is_some() { if ui.small_button("").clicked() {
job.should_remove = true; if job.handle.is_some() {
if let Err(e) = job.cancel.send(()) { if let Err(e) = job.cancel.send(()) {
log::error!("Failed to cancel job: {e:?}"); log::error!("Failed to cancel job: {e:?}");
}
} else {
remove_job = Some(job.id);
} }
}
});
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = format!("{:#}", err);
ui.colored_label(
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
)
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else { } else {
format!("{:width$}", &status.status, width = STATUS_LENGTH) remove_job = Some(job.id);
}) }
.on_hover_text_at_pointer(&status.status);
} }
}); });
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = format!("{:#}", err);
ui.colored_label(
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
)
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = err_string);
}
});
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
})
.on_hover_text_at_pointer(&status.status)
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = status.status.clone());
}
});
}
}
if !any_jobs {
ui.label("No jobs");
} }
if let Some(idx) = remove_job { if let Some(idx) = remove_job {
jobs.remove(idx); jobs.remove(idx);
} }
} }
struct JobStatusDisplay {
title: String,
progress_items: Option<[u32; 2]>,
error: bool,
}
impl From<&JobStatus> for JobStatusDisplay {
fn from(status: &JobStatus) -> Self {
Self {
title: status.title.clone(),
progress_items: status.progress_items,
error: status.error.is_some(),
}
}
}
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
ui.label("Jobs:");
let mut statuses = Vec::new();
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
statuses.push(JobStatusDisplay::from(&*status));
}
let running_jobs = statuses.iter().filter(|s| !s.error).count();
let error_jobs = statuses.iter().filter(|s| s.error).count();
let mut clicked = false;
let spinner =
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
match running_jobs.cmp(&1) {
Ordering::Equal => {
spinner.ui(ui);
let running_job = statuses.iter().find(|s| !s.error).unwrap();
let text = if let Some(items) = running_job.progress_items {
format!("{} ({}/{})", running_job.title, items[0], items[1])
} else {
running_job.title.clone()
};
clicked |= ui.link(RichText::new(text)).clicked();
}
Ordering::Greater => {
spinner.ui(ui);
clicked |= ui.link(format!("{} running", running_jobs)).clicked();
}
_ => (),
}
match error_jobs.cmp(&1) {
Ordering::Equal => {
let error_job = statuses.iter().find(|s| s.error).unwrap();
clicked |= ui
.link(
RichText::new(format!("{} error", error_job.title))
.color(appearance.delete_color),
)
.clicked();
}
Ordering::Greater => {
clicked |= ui
.link(
RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color),
)
.clicked();
}
_ => (),
}
if running_jobs == 0 && error_jobs == 0 {
clicked |= ui.link("None").clicked();
}
clicked
}
pub fn jobs_window(
ctx: &egui::Context,
show: &mut bool,
jobs: &mut JobQueue,
appearance: &Appearance,
) {
egui::Window::new("Jobs").open(show).show(ctx, |ui| {
jobs_ui(ui, jobs, appearance);
});
}

View File

@@ -1,6 +1,7 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat}; use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance; pub(crate) mod appearance;
pub(crate) mod column_layout;
pub(crate) mod config; pub(crate) mod config;
pub(crate) mod data_diff; pub(crate) mod data_diff;
pub(crate) mod debug; pub(crate) mod debug;

File diff suppressed because it is too large Load Diff