Compare commits

..

13 Commits

Author SHA1 Message Date
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
24 changed files with 885 additions and 514 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

284
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",
@@ -501,7 +495,7 @@ checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -565,9 +559,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 +900,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 +1008,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 +1019,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 +1059,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 +1075,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 +1094,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 +1112,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 +1125,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 +1148,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 +1209,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1236,7 +1230,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1247,14 +1241,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 +1264,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 +1394,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 +1461,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1554,7 +1548,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -1662,8 +1656,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 +1791,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 +1813,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 +1909,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 +2018,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 +2066,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 +2080,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 +2223,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 +2252,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 +2289,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 +2349,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 +2384,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 +2391,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 +2620,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 +2852,14 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-cli" name = "objdiff-cli"
version = "2.1.0" version = "2.2.2"
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,7 +2874,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-core" name = "objdiff-core"
version = "2.1.0" version = "2.2.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arm-attr", "arm-attr",
@@ -2883,7 +2913,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-gui" name = "objdiff-gui"
version = "2.1.0" version = "2.2.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2904,6 +2934,7 @@ dependencies = [
"log", "log",
"notify", "notify",
"objdiff-core", "objdiff-core",
"open",
"path-slash", "path-slash",
"png", "png",
"pollster", "pollster",
@@ -2937,9 +2968,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 +3009,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3049,7 +3094,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 +3111,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 +3191,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3174,15 +3225,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 +3259,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 +3297,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 +3361,7 @@ dependencies = [
"prost", "prost",
"prost-types", "prost-types",
"regex", "regex",
"syn 2.0.77", "syn 2.0.79",
"tempfile", "tempfile",
] ]
@@ -3324,7 +3375,7 @@ dependencies = [
"itertools", "itertools",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3528,9 +3579,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 +3599,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 +3620,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 +3637,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 +3649,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 +3808,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 +3961,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3922,7 +3972,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -3945,7 +3995,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4181,7 +4231,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4212,9 +4262,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 +4292,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 +4329,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4460,7 +4510,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -4540,7 +4590,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 +4642,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 +4805,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 +4839,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 +5646,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 +5679,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.79",
] ]
[[package]] [[package]]
@@ -5661,7 +5711,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 +5723,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.2.2"
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

@@ -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

@@ -199,7 +199,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 +280,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

@@ -60,7 +60,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() { if metadata.module_name.is_some() || metadata.module_id.is_some() {
metadata.progress_categories = vec!["modules".to_string()]; metadata.progress_categories = vec!["modules".to_string()];
} else { } else {
metadata.progress_categories = vec!["dol".to_string()]; metadata.progress_categories = vec!["dol".to_string()];
} }
if metadata.complete.unwrap_or(false) { complete = metadata.complete.unwrap_or(false);
};
if complete {
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,6 +1,6 @@
use std::{ use std::{
fs::File, fs::File,
io::Read, io::{BufReader, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -124,6 +124,10 @@ impl ProjectObject {
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)]
@@ -156,7 +160,7 @@ pub struct ProjectConfigInfo {
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 +169,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

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;
@@ -39,7 +40,7 @@ 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, DiffViewState, View},
}, },
@@ -61,6 +62,7 @@ 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,
} }
/// The configuration for a single object file. /// The configuration for a single object file.
@@ -72,6 +74,7 @@ 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>,
} }
#[inline] #[inline]
@@ -82,6 +85,36 @@ 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 project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant,
}
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,
project_config_info: None,
last_mod_check: Instant::now(),
}
}
}
#[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 +149,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,30 +169,22 @@ 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;
@@ -187,33 +195,33 @@ impl AppConfig {
} }
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;
} }
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;
} }
pub fn set_selected_obj(&mut self, object: ObjectConfig) { pub fn set_selected_obj(&mut self, object: ObjectConfig) {
self.selected_obj = Some(object); self.config.selected_obj = Some(object);
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
} }
} }
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 +249,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,8 +345,8 @@ 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());
} }
@@ -345,23 +354,23 @@ impl App {
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(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 { if let Some(info) = &state.project_config_info {
if file_modified(&info.path, info.timestamp) { if file_modified(&info.path, info.timestamp) {
config.config_change = true; state.config_change = true;
} }
} }
if config.config_change { if state.config_change {
config.config_change = false; state.config_change = false;
match load_project_config(config) { match load_project_config(state) {
Ok(()) => config_state.load_error = None, Ok(()) => config_state.load_error = None,
Err(e) => { Err(e) => {
log::error!("Failed to load project config: {e}"); log::error!("Failed to load project config: {e}");
@@ -370,47 +379,50 @@ impl App {
} }
} }
if config.watcher_change { if state.watcher_change {
drop(self.watcher.take()); drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir { if let Some(project_dir) = &state.config.project_dir {
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then( match build_globset(&state.config.watch_patterns)
|globset| { .map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset) create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
}, }) {
) {
Ok(watcher) => self.watcher = Some(watcher), Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"), Err(e) => log::error!("Failed to create watcher: {e}"),
} }
config.watcher_change = false; state.watcher_change = false;
} }
} }
if config.obj_change { if state.obj_change {
*diff_state = Default::default(); *diff_state = Default::default();
if config.selected_obj.is_some() { if state.config.selected_obj.is_some() {
config.queue_build = true; state.queue_build = true;
} }
config.obj_change = false; state.obj_change = false;
} }
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes { if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
config.queue_build = true; state.queue_build = true;
} }
if let Some(result) = &diff_state.build { if let Some(result) = &diff_state.build {
if state.last_mod_check.elapsed().as_millis() >= 500 {
state.last_mod_check = Instant::now();
if let Some((obj, _)) = &result.first_obj { if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) { if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) { if file_modified(path, timestamp) {
config.queue_reload = true; state.queue_reload = true;
} }
} }
} }
if let Some((obj, _)) = &result.second_obj { if let Some((obj, _)) = &result.second_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) { if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) { if file_modified(path, timestamp) {
config.queue_reload = true; state.queue_reload = true;
}
} }
} }
} }
@@ -418,17 +430,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_config(&state.config)));
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_config(&state.config);
// 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 +468,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,6 +484,7 @@ impl eframe::App for App {
show_arch_config, show_arch_config,
show_debug, show_debug,
show_graphics, show_graphics,
show_jobs,
} = 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);
@@ -485,8 +501,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 +511,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 +549,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 +570,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,28 +578,32 @@ 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;
} }
}); });
ui.separator();
if jobs_menu_ui(ui, jobs, appearance) {
*show_jobs = !*show_jobs;
}
}); });
}); });
@@ -603,8 +623,7 @@ impl eframe::App for App {
} else { } else {
egui::SidePanel::left("side_panel").show(ctx, |ui| { 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);
}); });
}); });
@@ -613,21 +632,22 @@ impl eframe::App for App {
}); });
} }
project_window(ctx, config, show_project_config, config_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);
} }
/// Called by the frame work to save state before shutdown. /// Called by the frame work 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

@@ -61,6 +61,7 @@ impl ObjectConfigV0 {
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: None, complete: None,
scratch: None, scratch: None,
source_path: None,
} }
} }
} }

View File

@@ -4,7 +4,7 @@ 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;
#[derive(Clone)] #[derive(Clone)]
pub enum ProjectObjectNode { pub enum ProjectObjectNode {
@@ -64,30 +64,30 @@ fn build_nodes(
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;
config.custom_args = project_config.custom_args; state.config.custom_args = project_config.custom_args;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_base = project_config.build_base; state.config.build_base = project_config.build_base;
config.build_target = project_config.build_target; state.config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| { state.config.watch_patterns = project_config.watch_patterns.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.objects;
config.object_nodes = build_nodes( state.object_nodes = build_nodes(
&config.objects, &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.project_config_info = Some(info);
} }
Ok(()) Ok(())
} }

View File

@@ -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,11 +1,10 @@
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, ObjDiff},
obj::{read, ObjInfo}, obj::{read, ObjInfo},
@@ -91,13 +90,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,15 +136,23 @@ 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(
@@ -189,36 +189,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,19 +236,32 @@ 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()),
2, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
Some(read::read(target_path, &config.diff_obj_config).with_context(|| { step_idx += 1;
format!("Failed to read object '{}'", target_path.display()) match read::read(target_path, &config.diff_obj_config) {
})?) Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
} }
_ => None, _ => None,
}; };
@@ -248,22 +271,36 @@ fn run_build(
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 +311,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

@@ -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},
@@ -54,7 +54,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 +71,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);
@@ -96,6 +96,7 @@ impl ConfigViewState {
reverse_fn_order: None, reverse_fn_order: None,
complete: None, complete: None,
scratch: None, scratch: None,
source_path: 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);
@@ -106,6 +107,7 @@ impl ConfigViewState {
reverse_fn_order: None, reverse_fn_order: None,
complete: None, complete: None,
scratch: None, scratch: None,
source_path: None,
}); });
} }
} }
@@ -113,11 +115,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 +169,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
@@ -231,7 +234,7 @@ pub fn config_ui(
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 +257,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 +280,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,39 +312,45 @@ 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(
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_obj,
project_dir.as_deref(),
&node,
appearance,
node_open,
);
} }
}); });
} }
if new_selected_obj != *selected_obj { if new_selected_obj != *selected_obj {
if let Some(obj) = new_selected_obj { if let Some(obj) = new_selected_obj {
// Will set obj_changed, which will trigger a rebuild // Will set obj_changed, which will trigger a rebuild
config_guard.set_selected_obj(obj); state_guard.set_selected_obj(obj);
} }
} }
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_object(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<ObjectConfig>,
project_dir: Option<&Path>,
name: &str, name: &str,
object: &ProjectObject, object: &ProjectObject,
appearance: &Appearance, appearance: &Appearance,
@@ -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,11 +377,13 @@ fn display_object(
}) })
.color(color), .color(color),
) )
.ui(ui) .ui(ui);
.clicked(); if get_source_path(project_dir, object).is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
}
// Always recreate ObjectConfig if selected, in case the project config changed. // Always recreate ObjectConfig if selected, in case the project config changed.
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. // ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
if selected || clicked { if selected || response.clicked() {
*selected_obj = Some(ObjectConfig { *selected_obj = Some(ObjectConfig {
name: object_name.to_string(), name: object_name.to_string(),
target_path: object.target_path.clone(), target_path: object.target_path.clone(),
@@ -377,10 +391,31 @@ fn display_object(
reverse_fn_order: object.reverse_fn_order(), reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(), complete: object.complete(),
scratch: object.scratch.clone(), scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
}); });
} }
} }
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
}
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();
}
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum NodeOpen { enum NodeOpen {
#[default] #[default]
@@ -393,13 +428,14 @@ 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<ObjectConfig>,
project_dir: Option<&Path>,
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::File(name, object) => {
display_object(ui, selected_obj, name, object, appearance); display_object(ui, selected_obj, project_dir, name, object, 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.as_ref().map(|path| contains_node(node, path));
@@ -425,7 +461,7 @@ 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, node, appearance, node_open);
} }
}); });
} }
@@ -523,33 +559,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) = &config_state.load_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; config_state.load_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 +596,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 +612,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 +641,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 +678,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 +698,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 +742,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 +754,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 +795,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 +805,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 +829,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 +838,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 +863,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

@@ -26,17 +26,17 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
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();
} }

View File

@@ -509,6 +509,18 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
); );
} }
}); });
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()
{
state.queue_open_source_path = true;
}
}); });
ui.scope(|ui| { ui.scope(|ui| {

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,21 +1,29 @@
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.separator();
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(&status.title); ui.label(&status.title);
if ui.small_button("").clicked() { if ui.small_button("").clicked() {
if job.handle.is_some() { if job.handle.is_some() {
job.should_remove = true;
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:?}");
} }
@@ -40,19 +48,115 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance)
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7) format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
}, },
) )
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color)); .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 { } else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 { ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3]) format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else { } else {
format!("{:width$}", &status.status, width = STATUS_LENGTH) format!("{:width$}", &status.status, width = STATUS_LENGTH)
}) })
.on_hover_text_at_pointer(&status.status); .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

@@ -13,7 +13,7 @@ use objdiff_core::{
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder};
use crate::{ use crate::{
app::AppConfigRef, app::AppStateRef,
jobs::{ jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult}, create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult}, objdiff::{BuildStatus, ObjDiffResult},
@@ -52,6 +52,8 @@ pub struct DiffViewState {
pub scratch_available: bool, pub scratch_available: bool,
pub queue_scratch: bool, pub queue_scratch: bool,
pub scratch_running: bool, pub scratch_running: bool,
pub source_path_available: bool,
pub queue_open_source_path: bool,
} }
#[derive(Default)] #[derive(Default)]
@@ -64,7 +66,7 @@ pub struct SymbolViewState {
} }
impl DiffViewState { impl DiffViewState {
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| match result { jobs.results.retain_mut(|result| match result {
JobResult::ObjDiff(result) => { JobResult::ObjDiff(result) => {
self.build = take(result); self.build = take(result);
@@ -80,26 +82,29 @@ impl DiffViewState {
self.scratch_running = jobs.is_running(Job::CreateScratch); self.scratch_running = jobs.is_running(Job::CreateScratch);
self.symbol_state.disable_reverse_fn_order = false; self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() { if let Ok(state) = state.read() {
if let Some(obj_config) = &config.selected_obj { if let Some(obj_config) = &state.config.selected_obj {
if let Some(value) = obj_config.reverse_fn_order { if let Some(value) = obj_config.reverse_fn_order {
self.symbol_state.reverse_fn_order = value; self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true; self.symbol_state.disable_reverse_fn_order = true;
} }
self.source_path_available = obj_config.source_path.is_some();
} else {
self.source_path_available = false;
} }
self.scratch_available = CreateScratchConfig::is_available(&config); self.scratch_available = CreateScratchConfig::is_available(&state.config);
} }
} }
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 let Some(result) = take(&mut self.scratch) { if let Some(result) = take(&mut self.scratch) {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url))); ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
} }
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;
} }
} }
@@ -108,8 +113,8 @@ impl DiffViewState {
if let Some(function_name) = if let Some(function_name) =
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone()) self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone())
{ {
if let Ok(config) = config.read() { if let Ok(state) = state.read() {
match CreateScratchConfig::from_config(&config, function_name) { match CreateScratchConfig::from_config(&state.config, function_name) {
Ok(config) => { Ok(config) => {
jobs.push_once(Job::CreateScratch, || { jobs.push_once(Job::CreateScratch, || {
start_create_scratch(ctx, config) start_create_scratch(ctx, config)
@@ -122,6 +127,22 @@ impl DiffViewState {
} }
} }
} }
if self.queue_open_source_path {
self.queue_open_source_path = false;
if let Ok(state) = state.read() {
if let (Some(project_dir), Some(source_path)) = (
&state.config.project_dir,
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()),
) {
let source_path = project_dir.join(source_path);
log::info!("Opening file {}", source_path.display());
open::that_detached(source_path).unwrap_or_else(|err| {
log::error!("Failed to open source file: {err}");
});
}
}
}
} }
} }
@@ -431,7 +452,7 @@ fn symbol_list_ui(
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) { fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Copy command").clicked() { if !status.cmdline.is_empty() && ui.button("Copy command").clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline)); ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline));
} }
if ui.button("Copy log").clicked() { if ui.button("Copy log").clicked() {
@@ -444,9 +465,15 @@ fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
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);
if !status.cmdline.is_empty() {
ui.label(&status.cmdline); ui.label(&status.cmdline);
}
if !status.stdout.is_empty() {
ui.colored_label(appearance.replace_color, &status.stdout); ui.colored_label(appearance.replace_color, &status.stdout);
}
if !status.stderr.is_empty() {
ui.colored_label(appearance.delete_color, &status.stderr); ui.colored_label(appearance.delete_color, &status.stderr);
}
}); });
}); });
} }
@@ -518,10 +545,27 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|ui| { |ui| {
ui.set_width(column_width); ui.set_width(column_width);
ui.horizontal(|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.label("Build base:"); ui.label("Build base:");
});
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()
{
state.queue_open_source_path = true;
}
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if result.second_status.success { if result.second_status.success {
if result.second_obj.is_none() { if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing"); ui.colored_label(appearance.replace_color, "Missing");