mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-15 08:06:25 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 741d93e211 | |||
| 603dbd6882 | |||
| 6fb0a63de2 | |||
| ab2e84a2c6 | |||
| 9596051cb4 | |||
| a5d9d8282e | |||
|
|
3287a0f65c | ||
|
|
fab9c62dfb | ||
| 08cd768260 | |||
| 8acaaf528c | |||
| 6e881a74e1 | |||
| cc1bc44e69 | |||
| c7b85518ab | |||
| bb039a1445 | |||
| 8fc142d316 | |||
| b0123b3f83 | |||
| 2ec17aee9b | |||
| ec9731e1e5 | |||
|
|
a06382c27e | ||
| e013638c5a | |||
| 70ab82f1f7 | |||
| c5896689cf | |||
| 67719dd93e | |||
| 258e141017 | |||
| dbdda55065 | |||
|
|
a43320af1f | ||
|
|
35bbd40f5d | ||
|
|
c1cb4b0b19 | ||
| 2379853faa | |||
| 5e1aff180f |
5
.github/workflows/build.yaml
vendored
5
.github/workflows/build.yaml
vendored
@@ -110,11 +110,6 @@ jobs:
|
||||
name: linux-aarch64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: armv7-unknown-linux-musleabi
|
||||
name: linux-armv7l
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: windows-x86
|
||||
|
||||
1325
Cargo.lock
generated
1325
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0"
|
||||
version = "2.3.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -133,6 +133,13 @@
|
||||
},
|
||||
"metadata": {
|
||||
"ref": "#/$defs/metadata"
|
||||
},
|
||||
"symbol_mappings": {
|
||||
"type": "object",
|
||||
"description": "Manual symbol mappings from target to base.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,7 +11,6 @@ description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
@@ -29,3 +28,6 @@ supports-color = "3.0"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
mimalloc = "0.1"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
fn main() {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("Failed to execute git");
|
||||
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
||||
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
||||
}
|
||||
@@ -31,10 +31,9 @@ where T: FromArgs
|
||||
Ok(v) => {
|
||||
if v.version {
|
||||
println!(
|
||||
"{} {} {}",
|
||||
"{} {}",
|
||||
command_name.first().unwrap_or(&""),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("GIT_COMMIT_SHA"),
|
||||
);
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
|
||||
@@ -102,26 +102,32 @@ pub fn run(args: Args) -> Result<()> {
|
||||
let unit_path =
|
||||
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
|
||||
|
||||
let Some(object) = project_config.objects.iter_mut().find_map(|obj| {
|
||||
if obj.name.as_deref() == Some(u) {
|
||||
let Some(object) = project_config
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.iter_mut()
|
||||
.find_map(|obj| {
|
||||
if obj.name.as_deref() == Some(u) {
|
||||
resolve_paths(obj);
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
let up = unit_path.as_deref()?;
|
||||
|
||||
resolve_paths(obj);
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
let up = unit_path.as_deref()?;
|
||||
if [&obj.base_path, &obj.target_path]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||
.any(|p| p == up)
|
||||
{
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
resolve_paths(obj);
|
||||
|
||||
if [&obj.base_path, &obj.target_path]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||
.any(|p| p == up)
|
||||
{
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
None
|
||||
}) else {
|
||||
None
|
||||
})
|
||||
else {
|
||||
bail!("Unit not found: {}", u)
|
||||
};
|
||||
|
||||
@@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> {
|
||||
} else if let Some(symbol_name) = &args.symbol {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in project_config.objects.iter_mut().enumerate() {
|
||||
for (i, obj) in project_config
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
{
|
||||
resolve_paths(obj);
|
||||
|
||||
if obj
|
||||
@@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
match (count, idx) {
|
||||
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||
(1, Some(i)) => &mut project_config.objects[i],
|
||||
(1, Some(i)) => &mut project_config.units_mut()[i],
|
||||
(2.., Some(_)) => bail!(
|
||||
"Multiple instances of {} were found, try specifying a unit",
|
||||
symbol_name
|
||||
@@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
struct FunctionDiffUi {
|
||||
relax_reloc_diffs: bool,
|
||||
left_highlight: HighlightKind,
|
||||
@@ -758,7 +770,7 @@ impl FunctionDiffUi {
|
||||
self.scroll_y += self.per_page / if half { 2 } else { 1 };
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn print_sym(
|
||||
&self,
|
||||
out: &mut Text<'static>,
|
||||
|
||||
@@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
};
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
project.objects.len(),
|
||||
project.units().len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
@@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
for object in &mut project.objects {
|
||||
for object in project.units.as_deref_mut().unwrap_or_default() {
|
||||
if let Some(unit) = report_object(
|
||||
object,
|
||||
project_dir,
|
||||
@@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
let vec = project
|
||||
.objects
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.par_iter_mut()
|
||||
.map(|object| {
|
||||
report_object(
|
||||
@@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
}
|
||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||
let mut categories = Vec::new();
|
||||
for category in &project.progress_categories {
|
||||
for category in project.progress_categories() {
|
||||
categories.push(ReportCategory {
|
||||
id: category.id.clone(),
|
||||
name: category.name.clone(),
|
||||
@@ -199,7 +201,7 @@ fn report_object(
|
||||
.unwrap_or_default(),
|
||||
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 functions = vec![];
|
||||
|
||||
@@ -237,7 +239,7 @@ fn report_object(
|
||||
}
|
||||
|
||||
for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) {
|
||||
if symbol.size == 0 {
|
||||
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
|
||||
continue;
|
||||
}
|
||||
if let Some(existing_functions) = &mut existing_functions {
|
||||
@@ -280,6 +282,7 @@ fn report_object(
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
measures.complete_code = measures.total_code;
|
||||
measures.complete_data = measures.total_data;
|
||||
measures.complete_units = 1;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
|
||||
@@ -2,6 +2,12 @@ mod argp_version;
|
||||
mod cmd;
|
||||
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 anyhow::{Error, Result};
|
||||
|
||||
@@ -17,8 +17,8 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
||||
config = ["globset", "semver", "serde_json", "serde_yaml"]
|
||||
any-arch = ["bimap"] # Implicit, used to check if any arch is enabled
|
||||
config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"]
|
||||
dwarf = ["gimli"]
|
||||
mips = ["any-arch", "rabbitizer"]
|
||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||
@@ -27,8 +27,12 @@ arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
||||
bindings = ["serde_json", "prost", "pbjson"]
|
||||
wasm = ["bindings", "console_error_panic_hook", "console_log"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bimap = { version = "0.6", features = ["serde"], optional = true }
|
||||
byteorder = "1.5"
|
||||
filetime = "0.2"
|
||||
flagset = "0.4"
|
||||
@@ -57,7 +61,7 @@ gimli = { version = "0.31", default-features = false, features = ["read-all"], o
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
cwextab = { version = "0.2", optional = true }
|
||||
cwextab = { version = "1.0.2", optional = true }
|
||||
ppc750cl = { version = "0.3", optional = true }
|
||||
|
||||
# mips
|
||||
|
||||
Binary file not shown.
@@ -32,6 +32,10 @@ message Measures {
|
||||
uint64 complete_data = 13;
|
||||
// Completed (or "linked") data percent
|
||||
float complete_data_percent = 14;
|
||||
// Total number of units
|
||||
uint32 total_units = 15;
|
||||
// Completed (or "linked") units
|
||||
uint32 complete_units = 16;
|
||||
}
|
||||
|
||||
// Project progress report
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use byteorder::ByteOrder;
|
||||
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
|
||||
|
||||
use crate::{
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjReloc, ObjSection},
|
||||
util::ReallySigned,
|
||||
};
|
||||
|
||||
#[cfg(feature = "arm")]
|
||||
@@ -17,6 +19,97 @@ pub mod ppc;
|
||||
#[cfg(feature = "x86")]
|
||||
pub mod x86;
|
||||
|
||||
/// Represents the type of data associated with an instruction
|
||||
pub enum DataType {
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
Int128,
|
||||
Float,
|
||||
Double,
|
||||
Bytes,
|
||||
String,
|
||||
}
|
||||
|
||||
impl DataType {
|
||||
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
|
||||
if self.required_len().is_some_and(|l| bytes.len() < l) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match self {
|
||||
DataType::Int8 => {
|
||||
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
|
||||
if i < 0 {
|
||||
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int8: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int16 => {
|
||||
let i = Endian::read_i16(bytes);
|
||||
if i < 0 {
|
||||
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int16: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int32 => {
|
||||
let i = Endian::read_i32(bytes);
|
||||
if i < 0 {
|
||||
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int32: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int64 => {
|
||||
let i = Endian::read_i64(bytes);
|
||||
if i < 0 {
|
||||
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int64: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int128 => {
|
||||
let i = Endian::read_i128(bytes);
|
||||
if i < 0 {
|
||||
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int128: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Float => {
|
||||
format!("Float: {}", Endian::read_f32(bytes))
|
||||
}
|
||||
DataType::Double => {
|
||||
format!("Double: {}", Endian::read_f64(bytes))
|
||||
}
|
||||
DataType::Bytes => {
|
||||
format!("Bytes: {:#?}", bytes)
|
||||
}
|
||||
DataType::String => {
|
||||
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn required_len(&self) -> Option<usize> {
|
||||
match self {
|
||||
DataType::Int8 => Some(1),
|
||||
DataType::Int16 => Some(2),
|
||||
DataType::Int32 => Some(4),
|
||||
DataType::Int64 => Some(8),
|
||||
DataType::Int128 => Some(16),
|
||||
DataType::Float => Some(4),
|
||||
DataType::Double => Some(8),
|
||||
DataType::Bytes => None,
|
||||
DataType::String => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ObjArch: Send + Sync {
|
||||
fn process_code(
|
||||
&self,
|
||||
@@ -42,6 +135,12 @@ pub trait ObjArch: Send + Sync {
|
||||
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
|
||||
|
||||
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
|
||||
|
||||
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||
Some(format!("Bytes: {:#x?}", bytes))
|
||||
}
|
||||
|
||||
// Downcast methods
|
||||
#[cfg(feature = "ppc")]
|
||||
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use byteorder::BigEndian;
|
||||
use cwextab::{decode_extab, ExceptionTableData};
|
||||
use object::{
|
||||
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
|
||||
Symbol, SymbolKind,
|
||||
};
|
||||
use ppc750cl::{Argument, InsIter, GPR};
|
||||
use ppc750cl::{Argument, InsIter, Opcode, GPR};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
arch::{DataType, ObjArch, ProcessCodeResult},
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
|
||||
};
|
||||
@@ -186,6 +187,34 @@ impl ObjArch for ObjArchPpc {
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
|
||||
// Always shows the first string of the table. Not ideal, but it's really hard to find
|
||||
// the actual string being referenced.
|
||||
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
|
||||
return Some(DataType::String);
|
||||
}
|
||||
|
||||
match Opcode::from(instruction.op as u8) {
|
||||
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
|
||||
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
|
||||
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
|
||||
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
|
||||
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
|
||||
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
|
||||
|
||||
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
|
||||
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
|
||||
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
|
||||
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
|
||||
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||
ty.display_bytes::<BigEndian>(bytes)
|
||||
}
|
||||
|
||||
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
|
||||
}
|
||||
|
||||
@@ -303,9 +332,13 @@ fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, Excep
|
||||
continue;
|
||||
};
|
||||
let data = match decode_extab(extab_data) {
|
||||
Some(decoded_data) => decoded_data,
|
||||
None => {
|
||||
log::warn!("Exception table decoding failed for function {}", extab_func_name);
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Exception table decoding failed for function {}, reason: {}",
|
||||
extab_func_name,
|
||||
e.to_string()
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,9 +8,10 @@ use serde_json::error::Category;
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||
|
||||
pub const REPORT_VERSION: u32 = 1;
|
||||
pub const REPORT_VERSION: u32 = 2;
|
||||
|
||||
impl Report {
|
||||
/// Attempts to parse the report as binary protobuf or JSON.
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
if data.is_empty() {
|
||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||
@@ -25,6 +26,7 @@ impl 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> {
|
||||
match serde_json::from_slice::<Self>(bytes) {
|
||||
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<()> {
|
||||
if self.version == 0 {
|
||||
self.migrate_v0()?;
|
||||
}
|
||||
if self.version == 1 {
|
||||
self.migrate_v1()?;
|
||||
}
|
||||
if self.version != REPORT_VERSION {
|
||||
bail!("Unsupported report version: {}", self.version);
|
||||
}
|
||||
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<()> {
|
||||
let Some(measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
@@ -61,15 +70,16 @@ impl Report {
|
||||
let Some(unit_measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let Some(metadata) = &mut unit.metadata else {
|
||||
bail!("Missing metadata in report unit");
|
||||
let mut complete = false;
|
||||
if let Some(metadata) = &mut unit.metadata {
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
complete = metadata.complete.unwrap_or(false);
|
||||
};
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
if complete {
|
||||
unit_measures.complete_code = unit_measures.total_code;
|
||||
unit_measures.complete_data = unit_measures.total_data;
|
||||
unit_measures.complete_code_percent = 100.0;
|
||||
@@ -84,10 +94,42 @@ impl Report {
|
||||
measures.complete_data += unit_measures.complete_data;
|
||||
}
|
||||
measures.calc_matched_percent();
|
||||
self.calculate_progress_categories();
|
||||
self.version = 1;
|
||||
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) {
|
||||
for unit in &self.units {
|
||||
let Some(metadata) = unit.metadata.as_ref() else {
|
||||
@@ -117,6 +159,72 @@ impl Report {
|
||||
measures.calc_matched_percent();
|
||||
}
|
||||
}
|
||||
|
||||
/// Split the report into multiple reports based on progress categories.
|
||||
/// Assumes progress categories are in the format `version`, `version.category`.
|
||||
/// This is a hack for projects that generate all versions in a single report.
|
||||
pub fn split(self) -> Vec<(String, Report)> {
|
||||
let mut reports = Vec::new();
|
||||
// Map units to Option to allow taking ownership
|
||||
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
|
||||
for category in &self.categories {
|
||||
if category.id.contains(".") {
|
||||
// Skip subcategories
|
||||
continue;
|
||||
}
|
||||
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
|
||||
id.starts_with(parent)
|
||||
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
|
||||
}
|
||||
let mut sub_categories = self
|
||||
.categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(&c.id, &category.id, '.'))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
// Remove category prefix
|
||||
for sub_category in &mut sub_categories {
|
||||
sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
|
||||
}
|
||||
let mut sub_units = units
|
||||
.iter_mut()
|
||||
.filter_map(|opt| {
|
||||
let unit = opt.as_mut()?;
|
||||
let metadata = unit.metadata.as_ref()?;
|
||||
if metadata.progress_categories.contains(&category.id) {
|
||||
opt.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for sub_unit in &mut sub_units {
|
||||
// Remove leading version/ from unit name
|
||||
if let Some(name) =
|
||||
sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
|
||||
{
|
||||
sub_unit.name = name.to_string();
|
||||
}
|
||||
// Filter progress categories
|
||||
let Some(metadata) = sub_unit.metadata.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
metadata.progress_categories = metadata
|
||||
.progress_categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(c, &category.id, '.'))
|
||||
.map(|c| c[category.id.len() + 1..].to_string())
|
||||
.collect();
|
||||
}
|
||||
reports.push((category.id.clone(), Report {
|
||||
measures: category.measures,
|
||||
units: sub_units,
|
||||
version: self.version,
|
||||
categories: sub_categories,
|
||||
}));
|
||||
}
|
||||
reports
|
||||
}
|
||||
}
|
||||
|
||||
impl Measures {
|
||||
@@ -176,6 +284,8 @@ impl AddAssign for Measures {
|
||||
self.matched_functions += other.matched_functions;
|
||||
self.complete_code += other.complete_code;
|
||||
self.complete_data += other.complete_data;
|
||||
self.total_units += other.total_units;
|
||||
self.complete_units += other.complete_units;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +1,100 @@
|
||||
use std::{
|
||||
fs,
|
||||
fs::File,
|
||||
io::Read,
|
||||
io::{BufReader, BufWriter, Read},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bimap::BiBTreeMap;
|
||||
use filetime::FileTime;
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectConfig {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub min_version: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom_make: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_dir: Option<PathBuf>,
|
||||
#[serde(default = "bool_true")]
|
||||
pub build_base: bool,
|
||||
#[serde(default)]
|
||||
pub build_target: bool,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_base: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_target: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub watch_patterns: Option<Vec<Glob>>,
|
||||
#[serde(default, alias = "units")]
|
||||
pub objects: Vec<ProjectObject>,
|
||||
#[serde(default)]
|
||||
pub progress_categories: Vec<ProjectProgressCategory>,
|
||||
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
|
||||
pub units: Option<Vec<ProjectObject>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
impl ProjectConfig {
|
||||
#[inline]
|
||||
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
|
||||
|
||||
#[inline]
|
||||
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
|
||||
self.units.get_or_insert_with(Vec::new)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
|
||||
self.progress_categories.as_deref().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
|
||||
self.progress_categories.get_or_insert_with(Vec::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectObject {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[deprecated(note = "Use metadata.complete")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<ProjectObjectMetadata>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub symbol_mappings: Option<SymbolMappings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
pub type SymbolMappings = BiBTreeMap<String, String>;
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectObjectMetadata {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub progress_categories: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_generated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectProgressCategory {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
@@ -112,32 +135,36 @@ impl ProjectObject {
|
||||
}
|
||||
|
||||
pub fn complete(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
||||
}
|
||||
|
||||
pub fn reverse_fn_order(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
||||
}
|
||||
|
||||
pub fn hidden(&self) -> bool {
|
||||
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)]
|
||||
pub struct ScratchConfig {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub platform: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub compiler: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub c_flags: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ctx_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub build_ctx: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_ctx: Option<bool>,
|
||||
}
|
||||
|
||||
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
||||
@@ -150,13 +177,13 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ProjectConfigInfo {
|
||||
pub path: PathBuf,
|
||||
pub timestamp: FileTime,
|
||||
pub timestamp: Option<FileTime>,
|
||||
}
|
||||
|
||||
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||
for filename in CONFIG_FILENAMES.iter() {
|
||||
let config_path = dir.join(filename);
|
||||
let Ok(mut file) = File::open(&config_path) else {
|
||||
let Ok(file) = File::open(&config_path) else {
|
||||
continue;
|
||||
};
|
||||
let metadata = file.metadata();
|
||||
@@ -165,9 +192,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
||||
continue;
|
||||
}
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut result = match filename.contains("json") {
|
||||
true => read_json_config(&mut file),
|
||||
false => read_yml_config(&mut file),
|
||||
true => read_json_config(&mut reader),
|
||||
false => read_yml_config(&mut reader),
|
||||
};
|
||||
if let Ok(config) = &result {
|
||||
// Validate min_version if present
|
||||
@@ -175,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
||||
result = Err(e);
|
||||
}
|
||||
}
|
||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts }));
|
||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn save_project_config(
|
||||
config: &ProjectConfig,
|
||||
info: &ProjectConfigInfo,
|
||||
) -> Result<ProjectConfigInfo> {
|
||||
if let Some(last_ts) = info.timestamp {
|
||||
// Check if the file has changed since we last read it
|
||||
if let Ok(metadata) = fs::metadata(&info.path) {
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
if ts != last_ts {
|
||||
return Err(anyhow!("Config file has changed since last read"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut writer =
|
||||
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
|
||||
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
|
||||
match ext {
|
||||
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
|
||||
"yml" | "yaml" => {
|
||||
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
|
||||
}
|
||||
_ => Err(anyhow!("Unknown config file extension: {ext}")),
|
||||
}?;
|
||||
let file = writer.into_inner().context("Failed to flush file")?;
|
||||
let metadata = file.metadata().context("Failed to get file metadata")?;
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
|
||||
}
|
||||
|
||||
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
||||
let Some(min_version) = &config.min_version else { return Ok(()) };
|
||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
|
||||
});
|
||||
}
|
||||
resolve_branches(&mut diff);
|
||||
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None })
|
||||
Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
|
||||
}
|
||||
|
||||
pub fn diff_code(
|
||||
@@ -67,7 +67,7 @@ pub fn diff_code(
|
||||
right.arg_diff = result.right_args_diff;
|
||||
}
|
||||
|
||||
let total = left_out.insts.len();
|
||||
let total = left_out.insts.len().max(right_out.insts.len());
|
||||
let percent = if diff_state.diff_count >= total {
|
||||
0.0
|
||||
} else {
|
||||
@@ -77,13 +77,13 @@ pub fn diff_code(
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: left_diff,
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: right_diff,
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
@@ -211,7 +211,7 @@ fn arg_eq(
|
||||
left_diff: &ObjInsDiff,
|
||||
right_diff: &ObjInsDiff,
|
||||
) -> bool {
|
||||
return match left {
|
||||
match left {
|
||||
ObjInsArg::PlainText(l) => match right {
|
||||
ObjInsArg::PlainText(r) => l == r,
|
||||
_ => false,
|
||||
@@ -236,7 +236,7 @@ fn arg_eq(
|
||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -20,13 +20,13 @@ pub fn diff_bss_symbol(
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
@@ -34,7 +34,7 @@ pub fn diff_bss_symbol(
|
||||
}
|
||||
|
||||
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
||||
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None }
|
||||
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
|
||||
}
|
||||
|
||||
/// Compare the data sections of two object files.
|
||||
@@ -158,13 +158,13 @@ pub fn diff_data_symbol(
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ pub enum DiffText<'a> {
|
||||
Eol,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum HighlightKind {
|
||||
#[default]
|
||||
None,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::HashSet;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
config::SymbolMappings,
|
||||
diff::{
|
||||
code::{diff_code, no_diff_code, process_code_symbol},
|
||||
data::{
|
||||
@@ -161,6 +162,8 @@ pub struct DiffObjConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub space_between_args: bool,
|
||||
pub combine_data_sections: bool,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: MappingConfig,
|
||||
// x86
|
||||
pub x86_formatter: X86Formatter,
|
||||
// MIPS
|
||||
@@ -182,6 +185,7 @@ impl Default for DiffObjConfig {
|
||||
relax_reloc_diffs: false,
|
||||
space_between_args: true,
|
||||
combine_data_sections: false,
|
||||
symbol_mappings: Default::default(),
|
||||
x86_formatter: Default::default(),
|
||||
mips_abi: Default::default(),
|
||||
mips_instr_category: Default::default(),
|
||||
@@ -223,8 +227,10 @@ impl ObjSectionDiff {
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjSymbolDiff {
|
||||
/// The symbol ref this object
|
||||
pub symbol_ref: SymbolRef,
|
||||
pub diff_symbol: Option<SymbolRef>,
|
||||
/// The symbol ref in the _other_ object that this symbol was diffed against
|
||||
pub target_symbol: Option<SymbolRef>,
|
||||
pub instructions: Vec<ObjInsDiff>,
|
||||
pub match_percent: Option<f32>,
|
||||
}
|
||||
@@ -294,8 +300,13 @@ pub struct ObjInsBranchTo {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ObjDiff {
|
||||
/// A list of all section diffs in the object.
|
||||
pub sections: Vec<ObjSectionDiff>,
|
||||
/// Common BSS symbols don't live in a section, so they're stored separately.
|
||||
pub common: Vec<ObjSymbolDiff>,
|
||||
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
|
||||
/// that are being mapped to the other object.
|
||||
pub mapping_symbols: Vec<ObjSymbolDiff>,
|
||||
}
|
||||
|
||||
impl ObjDiff {
|
||||
@@ -303,13 +314,14 @@ impl ObjDiff {
|
||||
let mut result = Self {
|
||||
sections: Vec::with_capacity(obj.sections.len()),
|
||||
common: Vec::with_capacity(obj.common.len()),
|
||||
mapping_symbols: vec![],
|
||||
};
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
let mut symbols = Vec::with_capacity(section.symbols.len());
|
||||
for (symbol_idx, _) in section.symbols.iter().enumerate() {
|
||||
symbols.push(ObjSymbolDiff {
|
||||
symbol_ref: SymbolRef { section_idx, symbol_idx },
|
||||
diff_symbol: None,
|
||||
target_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
});
|
||||
@@ -328,7 +340,7 @@ impl ObjDiff {
|
||||
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
||||
result.common.push(ObjSymbolDiff {
|
||||
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
|
||||
diff_symbol: None,
|
||||
target_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
});
|
||||
@@ -378,7 +390,7 @@ pub fn diff_objs(
|
||||
right: Option<&ObjInfo>,
|
||||
prev: Option<&ObjInfo>,
|
||||
) -> Result<DiffObjsResult> {
|
||||
let symbol_matches = matching_symbols(left, right, prev)?;
|
||||
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
|
||||
let section_matches = matching_sections(left, right)?;
|
||||
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||
@@ -529,6 +541,17 @@ pub fn diff_objs(
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
|
||||
(right.as_mut(), left.as_mut())
|
||||
{
|
||||
if let Some(right_name) = &config.symbol_mappings.selecting_left {
|
||||
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
|
||||
}
|
||||
if let Some(left_name) = &config.symbol_mappings.selecting_right {
|
||||
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DiffObjsResult {
|
||||
left: left.map(|(_, o)| o),
|
||||
right: right.map(|(_, o)| o),
|
||||
@@ -536,6 +559,63 @@ pub fn diff_objs(
|
||||
})
|
||||
}
|
||||
|
||||
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
|
||||
/// symbols in the other object that match the selected symbol's section and kind. This allows
|
||||
/// us to display match percentages for all symbols in the other object that could be selected.
|
||||
fn generate_mapping_symbols(
|
||||
base_obj: &ObjInfo,
|
||||
base_name: &str,
|
||||
target_obj: &ObjInfo,
|
||||
target_out: &mut ObjDiff,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<()> {
|
||||
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref);
|
||||
let Some(base_section) = base_section else {
|
||||
return Ok(());
|
||||
};
|
||||
let base_code = match base_section.kind {
|
||||
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
|
||||
_ => None,
|
||||
};
|
||||
for (target_section_index, target_section) in
|
||||
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
|
||||
{
|
||||
for (target_symbol_index, _target_symbol) in
|
||||
target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind)
|
||||
{
|
||||
let target_symbol_ref =
|
||||
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
|
||||
match base_section.kind {
|
||||
ObjSectionKind::Code => {
|
||||
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
|
||||
let (left_diff, _right_diff) = diff_code(
|
||||
&target_code,
|
||||
base_code.as_ref().unwrap(),
|
||||
target_symbol_ref,
|
||||
base_symbol_ref,
|
||||
config,
|
||||
)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
ObjSectionKind::Data => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
ObjSectionKind::Bss => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct SymbolMatch {
|
||||
left: Option<SymbolRef>,
|
||||
@@ -551,19 +631,115 @@ struct SectionMatch {
|
||||
section_kind: ObjSectionKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MappingConfig {
|
||||
/// Manual symbol mappings
|
||||
pub mappings: SymbolMappings,
|
||||
/// The right object symbol name that we're selecting a left symbol for
|
||||
pub selecting_left: Option<String>,
|
||||
/// The left object symbol name that we're selecting a right symbol for
|
||||
pub selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_symbol_mappings(
|
||||
left: &ObjInfo,
|
||||
right: &ObjInfo,
|
||||
mapping_config: &MappingConfig,
|
||||
left_used: &mut HashSet<SymbolRef>,
|
||||
right_used: &mut HashSet<SymbolRef>,
|
||||
matches: &mut Vec<SymbolMatch>,
|
||||
) -> Result<()> {
|
||||
// If we're selecting a symbol to use as a comparison, mark it as used
|
||||
// This ensures that we don't match it to another symbol at any point
|
||||
if let Some(left_name) = &mapping_config.selecting_left {
|
||||
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
|
||||
left_used.insert(left_symbol);
|
||||
}
|
||||
}
|
||||
if let Some(right_name) = &mapping_config.selecting_right {
|
||||
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
|
||||
right_used.insert(right_symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply manual symbol mappings
|
||||
for (left_name, right_name) in &mapping_config.mappings {
|
||||
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
|
||||
continue;
|
||||
};
|
||||
if left_used.contains(&left_symbol) {
|
||||
continue;
|
||||
}
|
||||
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
|
||||
continue;
|
||||
};
|
||||
if right_used.contains(&right_symbol) {
|
||||
continue;
|
||||
}
|
||||
let left_section = &left.sections[left_symbol.section_idx];
|
||||
let right_section = &right.sections[right_symbol.section_idx];
|
||||
if left_section.kind != right_section.kind {
|
||||
log::warn!(
|
||||
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
|
||||
left_name,
|
||||
left_section.kind,
|
||||
right_name,
|
||||
right_section.kind
|
||||
);
|
||||
continue;
|
||||
}
|
||||
matches.push(SymbolMatch {
|
||||
left: Some(left_symbol),
|
||||
right: Some(right_symbol),
|
||||
prev: None, // TODO
|
||||
section_kind: left_section.kind,
|
||||
});
|
||||
left_used.insert(left_symbol);
|
||||
right_used.insert(right_symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find matching symbols between each object.
|
||||
fn matching_symbols(
|
||||
left: Option<&ObjInfo>,
|
||||
right: Option<&ObjInfo>,
|
||||
prev: Option<&ObjInfo>,
|
||||
mappings: &MappingConfig,
|
||||
) -> Result<Vec<SymbolMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
let mut left_used = HashSet::new();
|
||||
let mut right_used = HashSet::new();
|
||||
if let Some(left) = left {
|
||||
if let Some(right) = right {
|
||||
apply_symbol_mappings(
|
||||
left,
|
||||
right,
|
||||
mappings,
|
||||
&mut left_used,
|
||||
&mut right_used,
|
||||
&mut matches,
|
||||
)?;
|
||||
}
|
||||
for (section_idx, section) in left.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx, symbol_idx };
|
||||
if left_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(SymbolRef { section_idx, symbol_idx }),
|
||||
left: Some(symbol_ref),
|
||||
right: find_symbol(right, symbol, section, Some(&right_used)),
|
||||
prev: find_symbol(prev, symbol, section, None),
|
||||
section_kind: section.kind,
|
||||
@@ -575,8 +751,12 @@ fn matching_symbols(
|
||||
}
|
||||
}
|
||||
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx: left.sections.len(), symbol_idx };
|
||||
if left_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }),
|
||||
left: Some(symbol_ref),
|
||||
right: find_common_symbol(right, symbol),
|
||||
prev: find_common_symbol(prev, symbol),
|
||||
section_kind: ObjSectionKind::Bss,
|
||||
|
||||
@@ -112,6 +112,15 @@ pub struct ObjIns {
|
||||
pub orig: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum ObjSymbolKind {
|
||||
#[default]
|
||||
Unknown,
|
||||
Function,
|
||||
Object,
|
||||
Section,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSymbol {
|
||||
pub name: String,
|
||||
@@ -120,12 +129,14 @@ pub struct ObjSymbol {
|
||||
pub section_address: u64,
|
||||
pub size: u64,
|
||||
pub size_known: bool,
|
||||
pub kind: ObjSymbolKind,
|
||||
pub flags: ObjSymbolFlagSet,
|
||||
pub addend: i64,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
/// Original index in object symbol table
|
||||
pub original_index: Option<usize>,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct ObjInfo {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
use std::{collections::HashSet, fs, io::Cursor, mem::size_of, path::Path};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
io::Cursor,
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use filetime::FileTime;
|
||||
@@ -17,6 +23,7 @@ use crate::{
|
||||
obj::{
|
||||
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
ObjSymbolKind,
|
||||
},
|
||||
util::{read_u16, read_u32},
|
||||
};
|
||||
@@ -78,6 +85,23 @@ fn to_obj_symbol(
|
||||
let virtual_address = split_meta
|
||||
.and_then(|m| m.virtual_addresses.as_ref())
|
||||
.and_then(|v| v.get(symbol.index().0).cloned());
|
||||
|
||||
let bytes = symbol
|
||||
.section_index()
|
||||
.and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||
.and_then(|section| section.data().ok())
|
||||
.and_then(|data| {
|
||||
data.get(section_address as usize..(section_address + symbol.size()) as usize)
|
||||
})
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let kind = match symbol.kind() {
|
||||
SymbolKind::Text => ObjSymbolKind::Function,
|
||||
SymbolKind::Data => ObjSymbolKind::Object,
|
||||
SymbolKind::Section => ObjSymbolKind::Section,
|
||||
_ => ObjSymbolKind::Unknown,
|
||||
};
|
||||
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
demangled_name,
|
||||
@@ -85,10 +109,12 @@ fn to_obj_symbol(
|
||||
section_address,
|
||||
size: symbol.size(),
|
||||
size_known: symbol.size() != 0,
|
||||
kind,
|
||||
flags,
|
||||
addend,
|
||||
virtual_address,
|
||||
original_index: Some(symbol.index().0),
|
||||
bytes: bytes.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,50 +161,66 @@ fn symbols_by_section(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
name_counts: &mut HashMap<String, u32>,
|
||||
) -> Result<Vec<ObjSymbol>> {
|
||||
let mut result = Vec::<ObjSymbol>::new();
|
||||
for symbol in obj_file.symbols() {
|
||||
for symbol in section_symbols {
|
||||
if symbol.kind() == SymbolKind::Section {
|
||||
continue;
|
||||
}
|
||||
if let Some(index) = symbol.section().index() {
|
||||
if index.0 == section.orig_index {
|
||||
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||
// TODO strip local syms in diff?
|
||||
let name = symbol.name().context("Failed to process symbol name")?;
|
||||
if symbol.size() == 0 || name.starts_with("lbl_") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)?);
|
||||
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||
// TODO strip local syms in diff?
|
||||
let name = symbol.name().context("Failed to process symbol name")?;
|
||||
if symbol.size() == 0 || name.starts_with("lbl_") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(to_obj_symbol(arch, obj_file, symbol, 0, split_meta)?);
|
||||
}
|
||||
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
||||
let mut iter = result.iter_mut().peekable();
|
||||
while let Some(symbol) = iter.next() {
|
||||
if symbol.size == 0 {
|
||||
if symbol.kind == ObjSymbolKind::Unknown && symbol.size == 0 {
|
||||
if let Some(next_symbol) = iter.peek() {
|
||||
symbol.size = next_symbol.address - symbol.address;
|
||||
} else {
|
||||
symbol.size = (section.address + section.size) - symbol.address;
|
||||
}
|
||||
// Set symbol kind if we ended up with a non-zero size
|
||||
if symbol.size > 0 {
|
||||
symbol.kind = match section.kind {
|
||||
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.is_empty() {
|
||||
// Dummy symbol for empty sections
|
||||
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
|
||||
let current_count: u32 = *name_counts.get(§ion.name).unwrap();
|
||||
result.push(ObjSymbol {
|
||||
name: format!("[{}]", section.name),
|
||||
name: if current_count > 1 {
|
||||
format!("[{} ({})]", section.name, current_count)
|
||||
} else {
|
||||
format!("[{}]", section.name)
|
||||
},
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
section_address: 0,
|
||||
size: section.size,
|
||||
size_known: true,
|
||||
kind: match section.kind {
|
||||
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||
},
|
||||
flags: Default::default(),
|
||||
addend: 0,
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
@@ -196,49 +238,75 @@ fn common_symbols(
|
||||
.collect::<Result<Vec<ObjSymbol>>>()
|
||||
}
|
||||
|
||||
const LOW_PRIORITY_SYMBOLS: &[&str] =
|
||||
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
|
||||
|
||||
fn best_symbol<'r, 'data, 'file>(
|
||||
symbols: &'r [Symbol<'data, 'file>],
|
||||
address: u64,
|
||||
) -> Option<&'r Symbol<'data, 'file>> {
|
||||
let closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
|
||||
Ok(index) => Some(index),
|
||||
Err(index) => index.checked_sub(1),
|
||||
}?;
|
||||
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
|
||||
for symbol in symbols.iter().skip(closest_symbol_index) {
|
||||
if symbol.address() > address {
|
||||
break;
|
||||
}
|
||||
if symbol.kind() == SymbolKind::Section
|
||||
|| (symbol.size() > 0 && (symbol.address() + symbol.size()) <= address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// TODO priority ranking with visibility, etc
|
||||
if let Some(best) = best_symbol {
|
||||
if LOW_PRIORITY_SYMBOLS.contains(&best.name().unwrap_or_default())
|
||||
&& !LOW_PRIORITY_SYMBOLS.contains(&symbol.name().unwrap_or_default())
|
||||
{
|
||||
best_symbol = Some(symbol);
|
||||
}
|
||||
} else {
|
||||
best_symbol = Some(symbol);
|
||||
}
|
||||
}
|
||||
best_symbol
|
||||
}
|
||||
|
||||
fn find_section_symbol(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
target: &Symbol<'_, '_>,
|
||||
address: u64,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<ObjSymbol> {
|
||||
if let Some(symbol) = best_symbol(section_symbols, address) {
|
||||
return to_obj_symbol(
|
||||
arch,
|
||||
obj_file,
|
||||
symbol,
|
||||
address as i64 - symbol.address() as i64,
|
||||
split_meta,
|
||||
);
|
||||
}
|
||||
// Fallback to section symbol
|
||||
let section_index =
|
||||
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
||||
let section = obj_file.section_by_index(section_index)?;
|
||||
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
|
||||
for symbol in obj_file.symbols() {
|
||||
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
|
||||
continue;
|
||||
}
|
||||
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
|
||||
if symbol.address() < address
|
||||
&& symbol.size() != 0
|
||||
&& (closest_symbol.is_none()
|
||||
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
|
||||
{
|
||||
closest_symbol = Some(symbol);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return to_obj_symbol(arch, obj_file, &symbol, 0, split_meta);
|
||||
}
|
||||
let (name, offset) = closest_symbol
|
||||
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
|
||||
.or_else(|| section.name().map(|n| (n, section.address())).ok())
|
||||
.unwrap_or(("<unknown>", 0));
|
||||
let offset_addr = address - offset;
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
name: section.name()?.to_string(),
|
||||
demangled_name: None,
|
||||
address: offset,
|
||||
section_address: address - section.address(),
|
||||
address: section.address(),
|
||||
section_address: 0,
|
||||
size: 0,
|
||||
size_known: false,
|
||||
kind: ObjSymbolKind::Section,
|
||||
flags: Default::default(),
|
||||
addend: offset_addr as i64,
|
||||
addend: address as i64 - section.address() as i64,
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -246,6 +314,7 @@ fn relocations_by_section(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<Vec<ObjReloc>> {
|
||||
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
|
||||
@@ -289,7 +358,14 @@ fn relocations_by_section(
|
||||
}
|
||||
SymbolKind::Section => {
|
||||
ensure!(addend >= 0, "Negative addend in reloc: {addend}");
|
||||
find_section_symbol(arch, obj_file, &symbol, addend as u64, split_meta)
|
||||
find_section_symbol(
|
||||
arch,
|
||||
obj_file,
|
||||
section_symbols,
|
||||
&symbol,
|
||||
addend as u64,
|
||||
split_meta,
|
||||
)
|
||||
}
|
||||
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
|
||||
}?;
|
||||
@@ -513,6 +589,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
|
||||
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
||||
size: symbol.size,
|
||||
size_known: symbol.size_known,
|
||||
kind: symbol.kind,
|
||||
flags: symbol.flags,
|
||||
addend: symbol.addend,
|
||||
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
||||
@@ -521,6 +598,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
|
||||
None
|
||||
},
|
||||
original_index: symbol.original_index,
|
||||
bytes: symbol.bytes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -621,11 +699,28 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||
let arch = new_arch(&obj_file)?;
|
||||
let split_meta = split_meta(&obj_file)?;
|
||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||
let mut name_counts: HashMap<String, u32> = HashMap::new();
|
||||
for section in &mut sections {
|
||||
section.symbols =
|
||||
symbols_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
|
||||
section.relocations =
|
||||
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
|
||||
let mut symbols = obj_file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index() == Some(SectionIndex(section.orig_index)))
|
||||
.collect::<Vec<_>>();
|
||||
symbols.sort_by_key(|s| s.address());
|
||||
section.symbols = symbols_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
&symbols,
|
||||
split_meta.as_ref(),
|
||||
&mut name_counts,
|
||||
)?;
|
||||
section.relocations = relocations_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
&symbols,
|
||||
split_meta.as_ref(),
|
||||
)?;
|
||||
}
|
||||
if config.combine_data_sections {
|
||||
combine_data_sections(&mut sections)?;
|
||||
|
||||
@@ -29,10 +29,10 @@ bytes = "1.7"
|
||||
cfg-if = "1.0"
|
||||
const_format = "0.2"
|
||||
cwdemangle = "1.0"
|
||||
cwextab = "0.2"
|
||||
cwextab = "1.0.2"
|
||||
dirs = "5.0"
|
||||
egui = "0.28"
|
||||
egui_extras = "0.28"
|
||||
egui = "0.29"
|
||||
egui_extras = "0.29"
|
||||
filetime = "0.2"
|
||||
float-ord = "0.3"
|
||||
font-kit = "0.14"
|
||||
@@ -40,22 +40,23 @@ globset = { version = "0.4", features = ["serde1"] }
|
||||
log = "0.4"
|
||||
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
open = "5.3"
|
||||
png = "0.17"
|
||||
pollster = "0.3"
|
||||
regex = "1.10"
|
||||
rfd = { version = "0.14" } #, default-features = false, features = ['xdg-portal']
|
||||
regex = "1.11"
|
||||
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
|
||||
rlwinmdec = "1.0"
|
||||
ron = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shell-escape = "0.1"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
tempfile = "3.12"
|
||||
tempfile = "3.13"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
|
||||
# Keep version in sync with egui
|
||||
[dependencies.eframe]
|
||||
version = "0.28"
|
||||
version = "0.29"
|
||||
features = [
|
||||
"default_fonts",
|
||||
"persistence",
|
||||
@@ -66,7 +67,7 @@ default-features = false
|
||||
|
||||
# Keep version in sync with eframe
|
||||
[dependencies.wgpu]
|
||||
version = "0.20"
|
||||
version = "22.1"
|
||||
features = [
|
||||
"dx12",
|
||||
"metal",
|
||||
@@ -89,9 +90,6 @@ self_update = "0.41"
|
||||
path-slash = "0.2"
|
||||
winapi = "0.3"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
exec = "0.3"
|
||||
|
||||
@@ -106,4 +104,6 @@ tracing-wasm = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0"
|
||||
vergen-gitcl = { version = "1.0", features = ["build", "cargo"] }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
tauri-winres = "0.1"
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use vergen_gitcl::{BuildBuilder, CargoBuilder, Emitter, GitclBuilder};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?;
|
||||
let mut res = tauri_winres::WindowsResource::new();
|
||||
res.set_icon("assets/icon.ico");
|
||||
res.set_language(0x0409); // US English
|
||||
res.compile()?;
|
||||
}
|
||||
Emitter::default()
|
||||
.add_instructions(&BuildBuilder::all_build()?)?
|
||||
.add_instructions(&CargoBuilder::all_cargo()?)?
|
||||
.add_instructions(&GitclBuilder::all_git()?)?
|
||||
.emit()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use filetime::FileTime;
|
||||
@@ -14,7 +15,8 @@ use globset::{Glob, GlobSet};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use objdiff_core::{
|
||||
config::{
|
||||
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS,
|
||||
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
|
||||
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
|
||||
},
|
||||
diff::DiffObjConfig,
|
||||
};
|
||||
@@ -39,13 +41,12 @@ use crate::{
|
||||
frame_history::FrameHistory,
|
||||
function_diff::function_diff_ui,
|
||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||
jobs::jobs_ui,
|
||||
jobs::{jobs_menu_ui, jobs_window},
|
||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
||||
symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ViewState {
|
||||
pub jobs: JobQueue,
|
||||
pub config_state: ConfigViewState,
|
||||
@@ -61,10 +62,35 @@ pub struct ViewState {
|
||||
pub show_arch_config: bool,
|
||||
pub show_debug: bool,
|
||||
pub show_graphics: bool,
|
||||
pub show_jobs: bool,
|
||||
pub show_side_panel: bool,
|
||||
}
|
||||
|
||||
impl Default for ViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
jobs: Default::default(),
|
||||
config_state: Default::default(),
|
||||
demangle_state: Default::default(),
|
||||
rlwinm_decode_state: Default::default(),
|
||||
diff_state: Default::default(),
|
||||
graphics_state: Default::default(),
|
||||
frame_history: Default::default(),
|
||||
show_appearance_config: false,
|
||||
show_demangle: false,
|
||||
show_rlwinm_decode: false,
|
||||
show_project_config: false,
|
||||
show_arch_config: false,
|
||||
show_debug: false,
|
||||
show_graphics: false,
|
||||
show_jobs: false,
|
||||
show_side_panel: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for a single object file.
|
||||
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfig {
|
||||
pub name: String,
|
||||
pub target_path: Option<PathBuf>,
|
||||
@@ -72,6 +98,24 @@ pub struct ObjectConfig {
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
pub complete: Option<bool>,
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: SymbolMappings,
|
||||
}
|
||||
|
||||
impl From<&ProjectObject> for ObjectConfig {
|
||||
fn from(object: &ProjectObject) -> Self {
|
||||
Self {
|
||||
name: object.name().to_string(),
|
||||
target_path: object.target_path.clone(),
|
||||
base_path: object.base_path.clone(),
|
||||
reverse_fn_order: object.reverse_fn_order(),
|
||||
complete: object.complete(),
|
||||
scratch: object.scratch.clone(),
|
||||
source_path: object.source_path().cloned(),
|
||||
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -82,6 +126,46 @@ fn default_watch_patterns() -> Vec<Glob> {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub config: AppConfig,
|
||||
pub objects: Vec<ProjectObject>,
|
||||
pub object_nodes: Vec<ProjectObjectNode>,
|
||||
pub watcher_change: bool,
|
||||
pub config_change: bool,
|
||||
pub obj_change: bool,
|
||||
pub queue_build: bool,
|
||||
pub queue_reload: bool,
|
||||
pub current_project_config: Option<ProjectConfig>,
|
||||
pub project_config_info: Option<ProjectConfigInfo>,
|
||||
pub last_mod_check: Instant,
|
||||
/// The right object symbol name that we're selecting a left symbol for
|
||||
pub selecting_left: Option<String>,
|
||||
/// The left object symbol name that we're selecting a right symbol for
|
||||
pub selecting_right: Option<String>,
|
||||
pub config_error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: Default::default(),
|
||||
objects: vec![],
|
||||
object_nodes: vec![],
|
||||
watcher_change: false,
|
||||
config_change: false,
|
||||
obj_change: false,
|
||||
queue_build: false,
|
||||
queue_reload: false,
|
||||
current_project_config: None,
|
||||
project_config_info: None,
|
||||
last_mod_check: Instant::now(),
|
||||
selecting_left: None,
|
||||
selecting_right: None,
|
||||
config_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppConfig {
|
||||
// TODO: https://github.com/ron-rs/ron/pull/455
|
||||
@@ -116,23 +200,6 @@ pub struct AppConfig {
|
||||
pub recent_projects: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
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 {
|
||||
@@ -153,67 +220,174 @@ impl Default for AppConfig {
|
||||
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
||||
recent_projects: vec![],
|
||||
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) {
|
||||
self.recent_projects.retain(|p| p != &path);
|
||||
if self.recent_projects.len() > 9 {
|
||||
self.recent_projects.truncate(9);
|
||||
self.config.recent_projects.retain(|p| p != &path);
|
||||
if self.config.recent_projects.len() > 9 {
|
||||
self.config.recent_projects.truncate(9);
|
||||
}
|
||||
self.recent_projects.insert(0, path.clone());
|
||||
self.project_dir = Some(path);
|
||||
self.target_obj_dir = None;
|
||||
self.base_obj_dir = None;
|
||||
self.selected_obj = None;
|
||||
self.build_target = false;
|
||||
self.config.recent_projects.insert(0, path.clone());
|
||||
self.config.project_dir = Some(path);
|
||||
self.config.target_obj_dir = None;
|
||||
self.config.base_obj_dir = None;
|
||||
self.config.selected_obj = None;
|
||||
self.config.build_target = false;
|
||||
self.objects.clear();
|
||||
self.object_nodes.clear();
|
||||
self.watcher_change = true;
|
||||
self.config_change = true;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.current_project_config = None;
|
||||
self.project_config_info = None;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
||||
self.target_obj_dir = Some(path);
|
||||
self.selected_obj = None;
|
||||
self.config.target_obj_dir = Some(path);
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
||||
self.base_obj_dir = Some(path);
|
||||
self.selected_obj = None;
|
||||
self.config.base_obj_dir = Some(path);
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
|
||||
self.selected_obj = Some(object);
|
||||
pub fn set_selected_obj(&mut self, config: ObjectConfig) {
|
||||
if self.config.selected_obj.as_ref().is_some_and(|existing| existing == &config) {
|
||||
// Don't reload the object if there were no changes
|
||||
return;
|
||||
}
|
||||
self.config.selected_obj = Some(config);
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn clear_selected_obj(&mut self) {
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_selecting_left(&mut self, right: &str) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_right(right);
|
||||
self.selecting_left = Some(right.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn set_selecting_right(&mut self, left: &str) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_left(left);
|
||||
self.selecting_right = Some(left.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
log::warn!("No selected object");
|
||||
return;
|
||||
};
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
if left == right {
|
||||
object.symbol_mappings.remove_by_left(&left);
|
||||
object.symbol_mappings.remove_by_right(&right);
|
||||
} else {
|
||||
object.symbol_mappings.insert(left.clone(), right.clone());
|
||||
}
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
self.queue_reload = true;
|
||||
}
|
||||
|
||||
pub fn clear_mappings(&mut self) {
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
if let Some(object) = self.config.selected_obj.as_mut() {
|
||||
object.symbol_mappings.clear();
|
||||
}
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn is_selecting_symbol(&self) -> bool {
|
||||
self.selecting_left.is_some() || self.selecting_right.is_some()
|
||||
}
|
||||
|
||||
pub fn save_config(&mut self) {
|
||||
let (Some(config), Some(info)) =
|
||||
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Update the project config with the current state
|
||||
if let Some(object) = self.config.selected_obj.as_ref() {
|
||||
if let Some(existing) = config.units.as_mut().and_then(|v| {
|
||||
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||
}) {
|
||||
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object.symbol_mappings.clone())
|
||||
};
|
||||
}
|
||||
if let Some(existing) =
|
||||
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||
{
|
||||
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object.symbol_mappings.clone())
|
||||
};
|
||||
}
|
||||
}
|
||||
// Save the updated project config
|
||||
match save_project_config(config, info) {
|
||||
Ok(new_info) => *info = new_info,
|
||||
Err(e) => {
|
||||
log::error!("Failed to save project config: {e}");
|
||||
self.config_error = Some(format!("Failed to save project config: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
|
||||
pub type AppStateRef = Arc<RwLock<AppState>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
appearance: Appearance,
|
||||
view_state: ViewState,
|
||||
config: AppConfigRef,
|
||||
state: AppStateRef,
|
||||
modified: Arc<AtomicBool>,
|
||||
watcher: Option<notify::RecommendedWatcher>,
|
||||
app_path: Option<PathBuf>,
|
||||
@@ -241,16 +415,17 @@ impl App {
|
||||
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
||||
app.appearance = appearance;
|
||||
}
|
||||
if let Some(mut config) = deserialize_config(storage) {
|
||||
if config.project_dir.is_some() {
|
||||
config.config_change = true;
|
||||
config.watcher_change = true;
|
||||
if let Some(config) = deserialize_config(storage) {
|
||||
let mut state = AppState { config, ..Default::default() };
|
||||
if state.config.project_dir.is_some() {
|
||||
state.config_change = true;
|
||||
state.watcher_change = true;
|
||||
}
|
||||
if config.selected_obj.is_some() {
|
||||
config.queue_build = true;
|
||||
if state.config.selected_obj.is_some() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
app.view_state.config_state.queue_check_update = config.auto_update_check;
|
||||
app.config = Arc::new(RwLock::new(config));
|
||||
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
|
||||
app.state = Arc::new(RwLock::new(state));
|
||||
}
|
||||
}
|
||||
app.appearance.init_fonts(&cc.egui_ctx);
|
||||
@@ -336,81 +511,93 @@ impl App {
|
||||
jobs.results.append(&mut results);
|
||||
jobs.clear_finished();
|
||||
|
||||
diff_state.pre_update(jobs, &self.config);
|
||||
config_state.pre_update(jobs, &self.config);
|
||||
diff_state.pre_update(jobs, &self.state);
|
||||
config_state.pre_update(jobs, &self.state);
|
||||
debug_assert!(jobs.results.is_empty());
|
||||
}
|
||||
|
||||
fn post_update(&mut self, ctx: &egui::Context) {
|
||||
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
|
||||
self.appearance.post_update(ctx);
|
||||
|
||||
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
||||
config_state.post_update(ctx, jobs, &self.config);
|
||||
diff_state.post_update(ctx, jobs, &self.config);
|
||||
config_state.post_update(ctx, jobs, &self.state);
|
||||
diff_state.post_update(action, ctx, jobs, &self.state);
|
||||
|
||||
let Ok(mut config) = self.config.write() else {
|
||||
let Ok(mut state) = self.state.write() else {
|
||||
return;
|
||||
};
|
||||
let config = &mut *config;
|
||||
let state = &mut *state;
|
||||
|
||||
if let Some(info) = &config.project_config_info {
|
||||
if file_modified(&info.path, info.timestamp) {
|
||||
config.config_change = true;
|
||||
}
|
||||
let mut mod_check = false;
|
||||
if state.last_mod_check.elapsed().as_millis() >= 500 {
|
||||
state.last_mod_check = Instant::now();
|
||||
mod_check = true;
|
||||
}
|
||||
|
||||
if config.config_change {
|
||||
config.config_change = false;
|
||||
match load_project_config(config) {
|
||||
Ok(()) => config_state.load_error = None,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load project config: {e}");
|
||||
config_state.load_error = Some(format!("{e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.watcher_change {
|
||||
drop(self.watcher.take());
|
||||
|
||||
if let Some(project_dir) = &config.project_dir {
|
||||
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|
||||
|globset| {
|
||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||
.map_err(anyhow::Error::new)
|
||||
},
|
||||
) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||
}
|
||||
config.watcher_change = false;
|
||||
}
|
||||
}
|
||||
|
||||
if config.obj_change {
|
||||
*diff_state = Default::default();
|
||||
if config.selected_obj.is_some() {
|
||||
config.queue_build = true;
|
||||
}
|
||||
config.obj_change = false;
|
||||
}
|
||||
|
||||
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
|
||||
config.queue_build = true;
|
||||
}
|
||||
|
||||
if let Some(result) = &diff_state.build {
|
||||
if let Some((obj, _)) = &result.first_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
config.queue_reload = true;
|
||||
if mod_check {
|
||||
if let Some(info) = &state.project_config_info {
|
||||
if let Some(last_ts) = info.timestamp {
|
||||
if file_modified(&info.path, last_ts) {
|
||||
state.config_change = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((obj, _)) = &result.second_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
|
||||
if state.config_change {
|
||||
state.config_change = false;
|
||||
match load_project_config(state) {
|
||||
Ok(()) => state.config_error = None,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load project config: {e}");
|
||||
state.config_error = Some(format!("{e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.watcher_change {
|
||||
drop(self.watcher.take());
|
||||
|
||||
if let Some(project_dir) = &state.config.project_dir {
|
||||
match build_globset(&state.config.watch_patterns)
|
||||
.map_err(anyhow::Error::new)
|
||||
.and_then(|globset| {
|
||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||
.map_err(anyhow::Error::new)
|
||||
}) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||
}
|
||||
state.watcher_change = false;
|
||||
}
|
||||
}
|
||||
|
||||
if state.obj_change {
|
||||
*diff_state = Default::default();
|
||||
if state.config.selected_obj.is_some() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
state.obj_change = false;
|
||||
}
|
||||
|
||||
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
|
||||
state.queue_build = true;
|
||||
}
|
||||
|
||||
if let Some(result) = &diff_state.build {
|
||||
if mod_check {
|
||||
if let Some((obj, _)) = &result.first_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((obj, _)) = &result.second_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,17 +605,20 @@ impl App {
|
||||
|
||||
// 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.
|
||||
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
|
||||
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
|
||||
config.queue_build = false;
|
||||
config.queue_reload = false;
|
||||
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||
let mut diff_config = ObjDiffConfig::from_config(config);
|
||||
if state.queue_build
|
||||
&& state.config.selected_obj.is_some()
|
||||
&& !jobs.is_running(Job::ObjDiff)
|
||||
{
|
||||
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
|
||||
state.queue_build = false;
|
||||
state.queue_reload = false;
|
||||
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||
let mut diff_config = ObjDiffConfig::from_state(state);
|
||||
// Don't build, just reload the current files
|
||||
diff_config.build_base = false;
|
||||
diff_config.build_target = false;
|
||||
jobs.push(start_build(ctx, diff_config));
|
||||
config.queue_reload = false;
|
||||
state.queue_reload = false;
|
||||
}
|
||||
|
||||
if graphics_state.should_relaunch {
|
||||
@@ -453,7 +643,7 @@ impl eframe::App for App {
|
||||
|
||||
self.pre_update(ctx);
|
||||
|
||||
let Self { config, appearance, view_state, .. } = self;
|
||||
let Self { state, appearance, view_state, .. } = self;
|
||||
let ViewState {
|
||||
jobs,
|
||||
config_state,
|
||||
@@ -469,12 +659,27 @@ impl eframe::App for App {
|
||||
show_arch_config,
|
||||
show_debug,
|
||||
show_graphics,
|
||||
show_jobs,
|
||||
show_side_panel,
|
||||
} = view_state;
|
||||
|
||||
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||
|
||||
let side_panel_available = diff_state.current_view == View::SymbolDiff;
|
||||
|
||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
if ui
|
||||
.add_enabled(
|
||||
side_panel_available,
|
||||
egui::Button::new(if *show_side_panel { "⏴" } else { "⏵" }),
|
||||
)
|
||||
.on_hover_text("Toggle side panel")
|
||||
.clicked()
|
||||
{
|
||||
*show_side_panel = !*show_side_panel;
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("File", |ui| {
|
||||
#[cfg(debug_assertions)]
|
||||
if ui.button("Debug…").clicked() {
|
||||
@@ -485,8 +690,8 @@ impl eframe::App for App {
|
||||
*show_project_config = !*show_project_config;
|
||||
ui.close_menu();
|
||||
}
|
||||
let recent_projects = if let Ok(guard) = config.read() {
|
||||
guard.recent_projects.clone()
|
||||
let recent_projects = if let Ok(guard) = state.read() {
|
||||
guard.config.recent_projects.clone()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
@@ -495,12 +700,12 @@ impl eframe::App for App {
|
||||
} else {
|
||||
ui.menu_button("Recent Projects…", |ui| {
|
||||
if ui.button("Clear").clicked() {
|
||||
config.write().unwrap().recent_projects.clear();
|
||||
state.write().unwrap().config.recent_projects.clear();
|
||||
};
|
||||
ui.separator();
|
||||
for path in recent_projects {
|
||||
if ui.button(format!("{}", path.display())).clicked() {
|
||||
config.write().unwrap().set_project_dir(path);
|
||||
state.write().unwrap().set_project_dir(path);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
@@ -533,12 +738,12 @@ impl eframe::App for App {
|
||||
*show_arch_config = !*show_arch_config;
|
||||
ui.close_menu();
|
||||
}
|
||||
let mut config = config.write().unwrap();
|
||||
let mut state = state.write().unwrap();
|
||||
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.");
|
||||
if response.changed() {
|
||||
config.watcher_change = true;
|
||||
state.watcher_change = true;
|
||||
};
|
||||
ui.add_enabled(
|
||||
!diff_state.symbol_state.disable_reverse_fn_order,
|
||||
@@ -554,7 +759,7 @@ impl eframe::App for App {
|
||||
);
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut config.diff_obj_config.relax_reloc_diffs,
|
||||
&mut state.config.diff_obj_config.relax_reloc_diffs,
|
||||
"Relax relocation diffs",
|
||||
)
|
||||
.on_hover_text(
|
||||
@@ -562,72 +767,78 @@ impl eframe::App for App {
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut config.diff_obj_config.space_between_args,
|
||||
&mut state.config.diff_obj_config.space_between_args,
|
||||
"Space between args",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut config.diff_obj_config.combine_data_sections,
|
||||
&mut state.config.diff_obj_config.combine_data_sections,
|
||||
"Combine data sections",
|
||||
)
|
||||
.on_hover_text("Combines data sections with equal names.")
|
||||
.changed()
|
||||
{
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui.button("Clear custom symbol mappings").clicked() {
|
||||
state.clear_mappings();
|
||||
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
|
||||
state.queue_reload = true;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if jobs_menu_ui(ui, jobs, appearance) {
|
||||
*show_jobs = !*show_jobs;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||
if diff_state.current_view == View::FunctionDiff && build_success {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
function_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
data_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
extab_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else {
|
||||
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
||||
if side_panel_available {
|
||||
egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
config_ui(ui, config, show_project_config, config_state, appearance);
|
||||
jobs_ui(ui, jobs, appearance);
|
||||
config_ui(ui, state, show_project_config, config_state, appearance);
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
symbol_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
}
|
||||
|
||||
project_window(ctx, config, show_project_config, config_state, appearance);
|
||||
let mut action = None;
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||
action = if diff_state.current_view == View::FunctionDiff && build_success {
|
||||
function_diff_ui(ui, diff_state, appearance)
|
||||
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||
data_diff_ui(ui, diff_state, appearance)
|
||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||
extab_diff_ui(ui, diff_state, appearance)
|
||||
} else {
|
||||
symbol_diff_ui(ui, diff_state, appearance)
|
||||
};
|
||||
});
|
||||
|
||||
project_window(ctx, state, show_project_config, config_state, appearance);
|
||||
appearance_window(ctx, show_appearance_config, appearance);
|
||||
demangle_window(ctx, show_demangle, demangle_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);
|
||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||
jobs_window(ctx, show_jobs, jobs, appearance);
|
||||
|
||||
self.post_update(ctx);
|
||||
self.post_update(ctx, action);
|
||||
}
|
||||
|
||||
/// Called by the frame work to save state before shutdown.
|
||||
/// Called by the framework to save state before shutdown.
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
if let Ok(config) = self.config.read() {
|
||||
eframe::set_value(storage, CONFIG_KEY, &*config);
|
||||
if let Ok(state) = self.state.read() {
|
||||
eframe::set_value(storage, CONFIG_KEY, &state.config);
|
||||
}
|
||||
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ use std::path::PathBuf;
|
||||
|
||||
use eframe::Storage;
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
config::ScratchConfig,
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
};
|
||||
|
||||
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
|
||||
|
||||
@@ -11,7 +15,7 @@ pub struct AppConfigVersion {
|
||||
}
|
||||
|
||||
impl Default for AppConfigVersion {
|
||||
fn default() -> Self { Self { version: 1 } }
|
||||
fn default() -> Self { Self { version: 2 } }
|
||||
}
|
||||
|
||||
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
|
||||
@@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
|
||||
let str = storage.get_string(CONFIG_KEY)?;
|
||||
match ron::from_str::<AppConfigVersion>(&str) {
|
||||
Ok(version) => match version.version {
|
||||
1 => from_str::<AppConfig>(&str),
|
||||
2 => from_str::<AppConfig>(&str),
|
||||
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
|
||||
_ => {
|
||||
log::warn!("Unknown config version: {}", version.version);
|
||||
None
|
||||
@@ -44,6 +49,180 @@ where T: serde::de::DeserializeOwned {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ScratchConfigV1 {
|
||||
#[serde(default)]
|
||||
pub platform: Option<String>,
|
||||
#[serde(default)]
|
||||
pub compiler: Option<String>,
|
||||
#[serde(default)]
|
||||
pub c_flags: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ctx_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub build_ctx: bool,
|
||||
}
|
||||
|
||||
impl ScratchConfigV1 {
|
||||
fn into_config(self) -> ScratchConfig {
|
||||
ScratchConfig {
|
||||
platform: self.platform,
|
||||
compiler: self.compiler,
|
||||
c_flags: self.c_flags,
|
||||
ctx_path: self.ctx_path,
|
||||
build_ctx: self.build_ctx.then_some(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfigV1 {
|
||||
pub name: String,
|
||||
pub target_path: Option<PathBuf>,
|
||||
pub base_path: Option<PathBuf>,
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
pub complete: Option<bool>,
|
||||
pub scratch: Option<ScratchConfigV1>,
|
||||
pub source_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjectConfigV1 {
|
||||
fn into_config(self) -> ObjectConfig {
|
||||
ObjectConfig {
|
||||
name: self.name,
|
||||
target_path: self.target_path,
|
||||
base_path: self.base_path,
|
||||
reverse_fn_order: self.reverse_fn_order,
|
||||
complete: self.complete,
|
||||
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||
source_path: self.source_path,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DiffObjConfigV1 {
|
||||
pub relax_reloc_diffs: bool,
|
||||
#[serde(default = "bool_true")]
|
||||
pub space_between_args: bool,
|
||||
pub combine_data_sections: bool,
|
||||
// x86
|
||||
pub x86_formatter: X86Formatter,
|
||||
// MIPS
|
||||
pub mips_abi: MipsAbi,
|
||||
pub mips_instr_category: MipsInstrCategory,
|
||||
// ARM
|
||||
pub arm_arch_version: ArmArchVersion,
|
||||
pub arm_unified_syntax: bool,
|
||||
pub arm_av_registers: bool,
|
||||
pub arm_r9_usage: ArmR9Usage,
|
||||
pub arm_sl_usage: bool,
|
||||
pub arm_fp_usage: bool,
|
||||
pub arm_ip_usage: bool,
|
||||
}
|
||||
|
||||
impl Default for DiffObjConfigV1 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
relax_reloc_diffs: false,
|
||||
space_between_args: true,
|
||||
combine_data_sections: false,
|
||||
x86_formatter: Default::default(),
|
||||
mips_abi: Default::default(),
|
||||
mips_instr_category: Default::default(),
|
||||
arm_arch_version: Default::default(),
|
||||
arm_unified_syntax: true,
|
||||
arm_av_registers: false,
|
||||
arm_r9_usage: Default::default(),
|
||||
arm_sl_usage: false,
|
||||
arm_fp_usage: false,
|
||||
arm_ip_usage: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffObjConfigV1 {
|
||||
fn into_config(self) -> DiffObjConfig {
|
||||
DiffObjConfig {
|
||||
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||
space_between_args: self.space_between_args,
|
||||
combine_data_sections: self.combine_data_sections,
|
||||
x86_formatter: self.x86_formatter,
|
||||
mips_abi: self.mips_abi,
|
||||
mips_instr_category: self.mips_instr_category,
|
||||
arm_arch_version: self.arm_arch_version,
|
||||
arm_unified_syntax: self.arm_unified_syntax,
|
||||
arm_av_registers: self.arm_av_registers,
|
||||
arm_r9_usage: self.arm_r9_usage,
|
||||
arm_sl_usage: self.arm_sl_usage,
|
||||
arm_fp_usage: self.arm_fp_usage,
|
||||
arm_ip_usage: self.arm_ip_usage,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppConfigV1 {
|
||||
pub version: u32,
|
||||
#[serde(default)]
|
||||
pub custom_make: Option<String>,
|
||||
#[serde(default)]
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub selected_wsl_distro: Option<String>,
|
||||
#[serde(default)]
|
||||
pub project_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub target_obj_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub base_obj_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub selected_obj: Option<ObjectConfigV1>,
|
||||
#[serde(default = "bool_true")]
|
||||
pub build_base: bool,
|
||||
#[serde(default)]
|
||||
pub build_target: bool,
|
||||
#[serde(default = "bool_true")]
|
||||
pub rebuild_on_changes: bool,
|
||||
#[serde(default)]
|
||||
pub auto_update_check: bool,
|
||||
#[serde(default)]
|
||||
pub watch_patterns: Vec<Glob>,
|
||||
#[serde(default)]
|
||||
pub recent_projects: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub diff_obj_config: DiffObjConfigV1,
|
||||
}
|
||||
|
||||
impl AppConfigV1 {
|
||||
fn into_config(self) -> AppConfig {
|
||||
log::info!("Upgrading configuration from v1");
|
||||
AppConfig {
|
||||
custom_make: self.custom_make,
|
||||
custom_args: self.custom_args,
|
||||
selected_wsl_distro: self.selected_wsl_distro,
|
||||
project_dir: self.project_dir,
|
||||
target_obj_dir: self.target_obj_dir,
|
||||
base_obj_dir: self.base_obj_dir,
|
||||
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
|
||||
build_base: self.build_base,
|
||||
build_target: self.build_target,
|
||||
rebuild_on_changes: self.rebuild_on_changes,
|
||||
auto_update_check: self.auto_update_check,
|
||||
watch_patterns: self.watch_patterns,
|
||||
recent_projects: self.recent_projects,
|
||||
diff_obj_config: self.diff_obj_config.into_config(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfigV0 {
|
||||
pub name: String,
|
||||
@@ -59,8 +238,7 @@ impl ObjectConfigV0 {
|
||||
target_path: Some(self.target_path),
|
||||
base_path: Some(self.base_path),
|
||||
reverse_fn_order: self.reverse_fn_order,
|
||||
complete: None,
|
||||
scratch: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use anyhow::Result;
|
||||
use globset::Glob;
|
||||
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
||||
|
||||
use crate::app::AppConfig;
|
||||
use crate::app::{AppState, ObjectConfig};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ProjectObjectNode {
|
||||
File(String, Box<ProjectObject>),
|
||||
Unit(String, usize),
|
||||
Dir(String, Vec<ProjectObjectNode>),
|
||||
}
|
||||
|
||||
@@ -33,17 +33,18 @@ fn find_dir<'a>(
|
||||
}
|
||||
|
||||
fn build_nodes(
|
||||
objects: &[ProjectObject],
|
||||
units: &mut [ProjectObject],
|
||||
project_dir: &Path,
|
||||
target_obj_dir: Option<&Path>,
|
||||
base_obj_dir: Option<&Path>,
|
||||
) -> Vec<ProjectObjectNode> {
|
||||
let mut nodes = vec![];
|
||||
for object in objects {
|
||||
for (idx, unit) in units.iter_mut().enumerate() {
|
||||
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
||||
let mut out_nodes = &mut nodes;
|
||||
let path = if let Some(name) = &object.name {
|
||||
let path = if let Some(name) = &unit.name {
|
||||
Path::new(name)
|
||||
} else if let Some(path) = &object.path {
|
||||
} else if let Some(path) = &unit.path {
|
||||
path
|
||||
} else {
|
||||
continue;
|
||||
@@ -56,38 +57,48 @@ fn build_nodes(
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut object = Box::new(object.clone());
|
||||
object.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
||||
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||
out_nodes.push(ProjectObjectNode::File(filename, object));
|
||||
out_nodes.push(ProjectObjectNode::Unit(filename, idx));
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
|
||||
let Some(project_dir) = &config.project_dir else {
|
||||
pub fn load_project_config(state: &mut AppState) -> Result<()> {
|
||||
let Some(project_dir) = &state.config.project_dir else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some((result, info)) = try_project_config(project_dir) {
|
||||
let project_config = result?;
|
||||
config.custom_make = project_config.custom_make;
|
||||
config.custom_args = project_config.custom_args;
|
||||
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));
|
||||
config.build_base = project_config.build_base;
|
||||
config.build_target = project_config.build_target;
|
||||
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
|
||||
state.config.custom_make = project_config.custom_make.clone();
|
||||
state.config.custom_args = project_config.custom_args.clone();
|
||||
state.config.target_obj_dir =
|
||||
project_config.target_dir.as_deref().map(|p| project_dir.join(p));
|
||||
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
|
||||
state.config.build_base = project_config.build_base.unwrap_or(true);
|
||||
state.config.build_target = project_config.build_target.unwrap_or(false);
|
||||
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
});
|
||||
config.watcher_change = true;
|
||||
config.objects = project_config.objects;
|
||||
config.object_nodes = build_nodes(
|
||||
&config.objects,
|
||||
state.watcher_change = true;
|
||||
state.objects = project_config.units.clone().unwrap_or_default();
|
||||
state.object_nodes = build_nodes(
|
||||
&mut state.objects,
|
||||
project_dir,
|
||||
config.target_obj_dir.as_deref(),
|
||||
config.base_obj_dir.as_deref(),
|
||||
state.config.target_obj_dir.as_deref(),
|
||||
state.config.base_obj_dir.as_deref(),
|
||||
);
|
||||
config.project_config_info = Some(info);
|
||||
state.current_project_config = Some(project_config);
|
||||
state.project_config_info = Some(info);
|
||||
|
||||
// Reload selected object
|
||||
if let Some(selected_obj) = &state.config.selected_obj {
|
||||
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
|
||||
let config = ObjectConfig::from(obj);
|
||||
state.set_selected_obj(config);
|
||||
} else {
|
||||
state.clear_selected_obj();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl CreateScratchConfig {
|
||||
Ok(Self {
|
||||
build_config: BuildConfig::from_config(config),
|
||||
context_path: scratch_config.ctx_path.clone(),
|
||||
build_context: scratch_config.build_ctx,
|
||||
build_context: scratch_config.build_ctx.unwrap_or(false),
|
||||
compiler: scratch_config.compiler.clone().unwrap_or_default(),
|
||||
platform: scratch_config.platform.clone().unwrap_or_default(),
|
||||
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
|
||||
|
||||
@@ -53,7 +53,7 @@ impl JobQueue {
|
||||
}
|
||||
|
||||
/// Returns whether any job is running.
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
pub fn any_running(&self) -> bool {
|
||||
self.jobs.iter().any(|job| {
|
||||
if let Some(handle) = &job.handle {
|
||||
@@ -85,12 +85,15 @@ impl JobQueue {
|
||||
/// Clears all finished jobs.
|
||||
pub fn clear_finished(&mut self) {
|
||||
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.
|
||||
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 context: JobContext,
|
||||
pub cancel: Sender<()>,
|
||||
pub should_remove: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -163,7 +165,7 @@ fn start_job(
|
||||
});
|
||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||
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(
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str::from_utf8,
|
||||
sync::mpsc::Receiver,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use objdiff_core::{
|
||||
diff::{diff_objs, DiffObjConfig, ObjDiff},
|
||||
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
|
||||
obj::{read, ObjInfo},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, ObjectConfig},
|
||||
app::{AppConfig, AppState, ObjectConfig},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
};
|
||||
|
||||
@@ -61,16 +60,20 @@ pub struct ObjDiffConfig {
|
||||
pub build_target: bool,
|
||||
pub selected_obj: Option<ObjectConfig>,
|
||||
pub diff_obj_config: DiffObjConfig,
|
||||
pub selecting_left: Option<String>,
|
||||
pub selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjDiffConfig {
|
||||
pub(crate) fn from_config(config: &AppConfig) -> Self {
|
||||
pub(crate) fn from_state(state: &AppState) -> Self {
|
||||
Self {
|
||||
build_config: BuildConfig::from_config(config),
|
||||
build_base: config.build_base,
|
||||
build_target: config.build_target,
|
||||
selected_obj: config.selected_obj.clone(),
|
||||
diff_obj_config: config.diff_obj_config.clone(),
|
||||
build_config: BuildConfig::from_config(&state.config),
|
||||
build_base: state.config.build_base,
|
||||
build_target: state.config.build_target,
|
||||
selected_obj: state.config.selected_obj.clone(),
|
||||
diff_obj_config: state.config.diff_obj_config.clone(),
|
||||
selecting_left: state.selecting_left.clone(),
|
||||
selecting_right: state.selecting_right.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,13 +94,6 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
|
||||
..Default::default()
|
||||
};
|
||||
};
|
||||
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_args = config.custom_args.as_deref().unwrap_or(&[]);
|
||||
#[cfg(not(windows))]
|
||||
@@ -144,23 +140,38 @@ fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildSta
|
||||
cmdline.push(' ');
|
||||
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 stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
||||
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
||||
Ok(BuildStatus {
|
||||
success: output.status.code().unwrap_or(-1) == 0,
|
||||
cmdline,
|
||||
stdout: stdout.to_string(),
|
||||
stderr: stderr.to_string(),
|
||||
})
|
||||
let output = match command.output() {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
return BuildStatus {
|
||||
success: false,
|
||||
cmdline,
|
||||
stdout: Default::default(),
|
||||
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(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: ObjDiffConfig,
|
||||
mut config: ObjDiffConfig,
|
||||
) -> Result<Box<ObjDiffResult>> {
|
||||
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||
let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||
// Use the per-object symbol mappings, we don't set mappings globally
|
||||
config.diff_obj_config.symbol_mappings = MappingConfig {
|
||||
mappings: obj_config.symbol_mappings,
|
||||
selecting_left: config.selecting_left,
|
||||
selecting_right: config.selecting_right,
|
||||
};
|
||||
|
||||
let project_dir = config
|
||||
.build_config
|
||||
.project_dir
|
||||
@@ -189,36 +200,46 @@ fn run_build(
|
||||
None
|
||||
};
|
||||
|
||||
let mut total = 3;
|
||||
let mut total = 1;
|
||||
if config.build_target && target_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.build_base && base_path_rel.is_some() {
|
||||
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 => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building target {}", target_path_rel.display()),
|
||||
0,
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, target_path_rel)
|
||||
}
|
||||
_ => 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 => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building base {}", base_path_rel.display()),
|
||||
0,
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, base_path_rel)
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
@@ -226,44 +247,71 @@ fn run_build(
|
||||
|
||||
let time = OffsetDateTime::now_utc();
|
||||
|
||||
let first_obj =
|
||||
match &obj_config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {}", target_path_rel.unwrap().display()),
|
||||
2,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
Some(read::read(target_path, &config.diff_obj_config).with_context(|| {
|
||||
format!("Failed to read object '{}'", target_path.display())
|
||||
})?)
|
||||
let first_obj = match &obj_config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {}", target_path_rel.unwrap().display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
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
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let second_obj = match &obj_config.base_path {
|
||||
Some(base_path) if second_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading base {}", base_path_rel.unwrap().display()),
|
||||
3,
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
Some(
|
||||
read::read(base_path, &config.diff_obj_config)
|
||||
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
|
||||
)
|
||||
step_idx += 1;
|
||||
match read::read(base_path, &config.diff_obj_config) {
|
||||
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,
|
||||
};
|
||||
|
||||
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)?;
|
||||
|
||||
update_status(context, "Complete".to_string(), total, total, &cancel)?;
|
||||
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
|
||||
Ok(Box::new(ObjDiffResult {
|
||||
first_status,
|
||||
second_status,
|
||||
@@ -274,7 +322,7 @@ fn run_build(
|
||||
}
|
||||
|
||||
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)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::{
|
||||
use anyhow::{ensure, Result};
|
||||
use cfg_if::cfg_if;
|
||||
use time::UtcOffset;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
|
||||
|
||||
@@ -39,7 +40,16 @@ const APP_NAME: &str = "objdiff";
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() -> ExitCode {
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt::init();
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
// Default to info level
|
||||
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
|
||||
.from_env_lossy()
|
||||
// This module is noisy at info level
|
||||
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Because localtime_r is unsound in multithreaded apps,
|
||||
// we must call this before initializing eframe.
|
||||
@@ -48,8 +58,10 @@ fn main() -> ExitCode {
|
||||
|
||||
let app_path = std::env::current_exe().ok();
|
||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||
let mut native_options =
|
||||
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
|
||||
let mut native_options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
|
||||
..Default::default()
|
||||
};
|
||||
match load_icon() {
|
||||
Ok(data) => {
|
||||
native_options.viewport.icon = Some(Arc::new(data));
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct Appearance {
|
||||
pub ui_font: FontId,
|
||||
pub code_font: FontId,
|
||||
pub diff_colors: Vec<Color32>,
|
||||
pub theme: eframe::Theme,
|
||||
pub theme: egui::Theme,
|
||||
|
||||
// Applied by theme
|
||||
#[serde(skip)]
|
||||
@@ -56,7 +56,7 @@ impl Default for Appearance {
|
||||
ui_font: DEFAULT_UI_FONT,
|
||||
code_font: DEFAULT_CODE_FONT,
|
||||
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
|
||||
theme: eframe::Theme::Dark,
|
||||
theme: egui::Theme::Dark,
|
||||
text_color: Color32::GRAY,
|
||||
emphasized_text_color: Color32::LIGHT_GRAY,
|
||||
deemphasized_text_color: Color32::DARK_GRAY,
|
||||
@@ -98,7 +98,7 @@ impl Appearance {
|
||||
});
|
||||
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
|
||||
match self.theme {
|
||||
eframe::Theme::Dark => {
|
||||
egui::Theme::Dark => {
|
||||
style.visuals = egui::Visuals::dark();
|
||||
self.text_color = Color32::GRAY;
|
||||
self.emphasized_text_color = Color32::LIGHT_GRAY;
|
||||
@@ -108,7 +108,7 @@ impl Appearance {
|
||||
self.insert_color = Color32::GREEN;
|
||||
self.delete_color = Color32::from_rgb(200, 40, 41);
|
||||
}
|
||||
eframe::Theme::Light => {
|
||||
egui::Theme::Light => {
|
||||
style.visuals = egui::Visuals::light();
|
||||
self.text_color = Color32::GRAY;
|
||||
self.emphasized_text_color = Color32::DARK_GRAY;
|
||||
@@ -274,8 +274,8 @@ pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut
|
||||
egui::ComboBox::from_label("Theme")
|
||||
.selected_text(format!("{:?}", appearance.theme))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut appearance.theme, eframe::Theme::Dark, "Dark");
|
||||
ui.selectable_value(&mut appearance.theme, eframe::Theme::Light, "Light");
|
||||
ui.selectable_value(&mut appearance.theme, egui::Theme::Dark, "Dark");
|
||||
ui.selectable_value(&mut appearance.theme, egui::Theme::Light, "Light");
|
||||
});
|
||||
ui.separator();
|
||||
appearance.next_ui_font =
|
||||
|
||||
82
objdiff-gui/src/views/column_layout.rs
Normal file
82
objdiff-gui/src/views/column_layout.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use egui::{Align, Layout, Sense, Vec2};
|
||||
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
|
||||
|
||||
pub fn render_header(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||
) {
|
||||
let column_width = available_width / num_columns as f32;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||
for i in 0..num_columns {
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
add_contents(ui, i);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
pub fn render_table(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
row_height: f32,
|
||||
total_rows: usize,
|
||||
mut add_contents: impl FnMut(&mut TableRow, usize),
|
||||
) {
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
let column_width = available_width / num_columns as f32;
|
||||
let available_height = ui.available_height();
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(false)
|
||||
.cell_layout(Layout::left_to_right(Align::Min))
|
||||
.columns(Column::exact(column_width).clip(true), num_columns)
|
||||
.resizable(false)
|
||||
.auto_shrink([false, false])
|
||||
.min_scrolled_height(available_height)
|
||||
.sense(Sense::click());
|
||||
table.body(|body| {
|
||||
body.rows(row_height, total_rows, |mut row| {
|
||||
row.set_hovered(false); // Disable hover effect
|
||||
for i in 0..num_columns {
|
||||
add_contents(&mut row, i);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_strips(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||
) {
|
||||
let column_width = available_width / num_columns as f32;
|
||||
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|
||||
|mut strip| {
|
||||
for i in 0..num_columns {
|
||||
strip.cell(|ui| {
|
||||
ui.push_id(i, |ui| {
|
||||
add_contents(ui, i);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,12 +2,11 @@
|
||||
use std::string::FromUtf16Error;
|
||||
use std::{
|
||||
mem::take,
|
||||
path::{PathBuf, MAIN_SEPARATOR},
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
};
|
||||
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
use anyhow::{Context, Result};
|
||||
use const_format::formatcp;
|
||||
use egui::{
|
||||
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
|
||||
SelectableLabel, TextFormat, Widget,
|
||||
@@ -17,11 +16,10 @@ use objdiff_core::{
|
||||
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
|
||||
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
};
|
||||
use self_update::cargo_crate_version;
|
||||
use strum::{EnumMessage, VariantArray};
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, AppConfigRef, ObjectConfig},
|
||||
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
|
||||
config::ProjectObjectNode,
|
||||
jobs::{
|
||||
check_update::{start_check_update, CheckUpdateResult},
|
||||
@@ -45,7 +43,6 @@ pub struct ConfigViewState {
|
||||
pub build_running: bool,
|
||||
pub queue_build: bool,
|
||||
pub watch_pattern_text: String,
|
||||
pub load_error: Option<String>,
|
||||
pub object_search: String,
|
||||
pub filter_diffable: bool,
|
||||
pub filter_incomplete: bool,
|
||||
@@ -56,7 +53,7 @@ pub struct 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| {
|
||||
if let JobResult::CheckUpdate(result) = result {
|
||||
self.check_update = take(result);
|
||||
@@ -73,21 +70,21 @@ impl ConfigViewState {
|
||||
match self.file_dialog_state.poll() {
|
||||
FileDialogResult::None => {}
|
||||
FileDialogResult::ProjectDir(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_project_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::TargetDir(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_target_obj_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::BaseDir(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_base_obj_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::Object(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
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) {
|
||||
let target_path = target_dir.join(obj_path);
|
||||
@@ -95,9 +92,7 @@ impl ConfigViewState {
|
||||
name: obj_path.display().to_string(),
|
||||
target_path: Some(target_path),
|
||||
base_path: Some(path),
|
||||
reverse_fn_order: None,
|
||||
complete: None,
|
||||
scratch: None,
|
||||
..Default::default()
|
||||
});
|
||||
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
||||
let base_path = base_dir.join(obj_path);
|
||||
@@ -105,9 +100,7 @@ impl ConfigViewState {
|
||||
name: obj_path.display().to_string(),
|
||||
target_path: Some(path),
|
||||
base_path: Some(base_path),
|
||||
reverse_fn_order: None,
|
||||
complete: None,
|
||||
scratch: None,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -115,11 +108,11 @@ impl ConfigViewState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
|
||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||
if self.queue_build {
|
||||
self.queue_build = false;
|
||||
if let Ok(mut config) = config.write() {
|
||||
config.queue_build = true;
|
||||
if let Ok(mut state) = state.write() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,47 +162,43 @@ fn fetch_wsl2_distros() -> Vec<String> {
|
||||
|
||||
pub fn config_ui(
|
||||
ui: &mut egui::Ui,
|
||||
config: &AppConfigRef,
|
||||
state: &AppStateRef,
|
||||
show_config_window: &mut bool,
|
||||
state: &mut ConfigViewState,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut config_guard = config.write().unwrap();
|
||||
let AppConfig {
|
||||
target_obj_dir,
|
||||
base_obj_dir,
|
||||
selected_obj,
|
||||
auto_update_check,
|
||||
let mut state_guard = state.write().unwrap();
|
||||
let AppState {
|
||||
config:
|
||||
AppConfig {
|
||||
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
|
||||
},
|
||||
objects,
|
||||
object_nodes,
|
||||
..
|
||||
} = &mut *config_guard;
|
||||
} = &mut *state_guard;
|
||||
|
||||
ui.heading("Updates");
|
||||
ui.checkbox(auto_update_check, "Check for updates on startup");
|
||||
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
|
||||
state.queue_check_update = true;
|
||||
if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
|
||||
{
|
||||
config_state.queue_check_update = true;
|
||||
}
|
||||
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
|
||||
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
|
||||
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
|
||||
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
|
||||
ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
|
||||
});
|
||||
if let Some(result) = &state.check_update {
|
||||
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
|
||||
if let Some(result) = &config_state.check_update {
|
||||
ui.label(format!("Latest version: {}", result.latest_release.version));
|
||||
if result.update_available {
|
||||
ui.colored_label(appearance.insert_color, "Update available");
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(bin_name) = &result.found_binary {
|
||||
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(
|
||||
"Automatically download and replace the current build",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.queue_update = Some(bin_name.clone());
|
||||
config_state.queue_update = Some(bin_name.clone());
|
||||
}
|
||||
}
|
||||
if ui
|
||||
@@ -234,11 +223,14 @@ pub fn config_ui(
|
||||
}
|
||||
});
|
||||
|
||||
let mut new_selected_obj = selected_obj.clone();
|
||||
let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
|
||||
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
|
||||
});
|
||||
let mut new_selected_index = selected_index;
|
||||
if objects.is_empty() {
|
||||
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
||||
if ui.button("Select object").clicked() {
|
||||
state.file_dialog_state.queue(
|
||||
config_state.file_dialog_state.queue(
|
||||
|| {
|
||||
Box::pin(
|
||||
rfd::AsyncFileDialog::new()
|
||||
@@ -261,8 +253,8 @@ pub fn config_ui(
|
||||
ui.colored_label(appearance.delete_color, "Missing project settings");
|
||||
}
|
||||
} else {
|
||||
let had_search = !state.object_search.is_empty();
|
||||
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
|
||||
let had_search = !config_state.object_search.is_empty();
|
||||
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
|
||||
|
||||
let mut root_open = None;
|
||||
let mut node_open = NodeOpen::Default;
|
||||
@@ -284,19 +276,22 @@ pub fn config_ui(
|
||||
node_open = NodeOpen::Object;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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");
|
||||
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");
|
||||
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");
|
||||
});
|
||||
});
|
||||
if state.object_search.is_empty() {
|
||||
if config_state.object_search.is_empty() {
|
||||
if had_search {
|
||||
root_open = Some(true);
|
||||
node_open = NodeOpen::Object;
|
||||
@@ -313,45 +308,55 @@ pub fn config_ui(
|
||||
.open(root_open)
|
||||
.default_open(true)
|
||||
.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);
|
||||
for node in object_nodes.iter().filter_map(|node| {
|
||||
filter_node(
|
||||
objects,
|
||||
node,
|
||||
&search,
|
||||
state.filter_diffable,
|
||||
state.filter_incomplete,
|
||||
state.show_hidden,
|
||||
config_state.filter_diffable,
|
||||
config_state.filter_incomplete,
|
||||
config_state.show_hidden,
|
||||
)
|
||||
}) {
|
||||
display_node(ui, &mut new_selected_obj, &node, appearance, node_open);
|
||||
display_node(
|
||||
ui,
|
||||
&mut new_selected_index,
|
||||
project_dir.as_deref(),
|
||||
objects,
|
||||
&node,
|
||||
appearance,
|
||||
node_open,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if new_selected_obj != *selected_obj {
|
||||
if let Some(obj) = new_selected_obj {
|
||||
if new_selected_index != selected_index {
|
||||
if let Some(idx) = new_selected_index {
|
||||
// Will set obj_changed, which will trigger a rebuild
|
||||
config_guard.set_selected_obj(obj);
|
||||
let config = ObjectConfig::from(&objects[idx]);
|
||||
state_guard.set_selected_obj(config);
|
||||
}
|
||||
}
|
||||
if config_guard.selected_obj.is_some()
|
||||
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
|
||||
if state_guard.config.selected_obj.is_some()
|
||||
&& ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
config_state.queue_build = true;
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
fn display_object(
|
||||
fn display_unit(
|
||||
ui: &mut egui::Ui,
|
||||
selected_obj: &mut Option<ObjectConfig>,
|
||||
selected_obj: &mut Option<usize>,
|
||||
project_dir: Option<&Path>,
|
||||
name: &str,
|
||||
object: &ProjectObject,
|
||||
units: &[ProjectObject],
|
||||
index: usize,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let object_name = object.name();
|
||||
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
|
||||
let object = &units[index];
|
||||
let selected = *selected_obj == Some(index);
|
||||
let color = if selected {
|
||||
appearance.emphasized_text_color
|
||||
} else if let Some(complete) = object.complete() {
|
||||
@@ -363,7 +368,7 @@ fn display_object(
|
||||
} else {
|
||||
appearance.text_color
|
||||
};
|
||||
let clicked = SelectableLabel::new(
|
||||
let response = SelectableLabel::new(
|
||||
selected,
|
||||
RichText::new(name)
|
||||
.font(FontId {
|
||||
@@ -372,19 +377,32 @@ fn display_object(
|
||||
})
|
||||
.color(color),
|
||||
)
|
||||
.ui(ui)
|
||||
.clicked();
|
||||
// Always recreate ObjectConfig if selected, in case the project config changed.
|
||||
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
|
||||
if selected || clicked {
|
||||
*selected_obj = Some(ObjectConfig {
|
||||
name: object_name.to_string(),
|
||||
target_path: object.target_path.clone(),
|
||||
base_path: object.base_path.clone(),
|
||||
reverse_fn_order: object.reverse_fn_order(),
|
||||
complete: object.complete(),
|
||||
scratch: object.scratch.clone(),
|
||||
});
|
||||
.ui(ui);
|
||||
if get_source_path(project_dir, object).is_some() {
|
||||
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
|
||||
}
|
||||
if response.clicked() {
|
||||
*selected_obj = Some(index);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,17 +417,19 @@ enum NodeOpen {
|
||||
|
||||
fn display_node(
|
||||
ui: &mut egui::Ui,
|
||||
selected_obj: &mut Option<ObjectConfig>,
|
||||
selected_obj: &mut Option<usize>,
|
||||
project_dir: Option<&Path>,
|
||||
units: &[ProjectObject],
|
||||
node: &ProjectObjectNode,
|
||||
appearance: &Appearance,
|
||||
node_open: NodeOpen,
|
||||
) {
|
||||
match node {
|
||||
ProjectObjectNode::File(name, object) => {
|
||||
display_object(ui, selected_obj, name, object, appearance);
|
||||
ProjectObjectNode::Unit(name, idx) => {
|
||||
display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
|
||||
}
|
||||
ProjectObjectNode::Dir(name, children) => {
|
||||
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
|
||||
let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
|
||||
let open = match node_open {
|
||||
NodeOpen::Default => None,
|
||||
NodeOpen::Open => Some(true),
|
||||
@@ -432,16 +452,16 @@ fn display_node(
|
||||
.open(open)
|
||||
.show(ui, |ui| {
|
||||
for node in children {
|
||||
display_node(ui, selected_obj, node, appearance, node_open);
|
||||
display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool {
|
||||
fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
|
||||
match node {
|
||||
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name,
|
||||
ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
|
||||
ProjectObjectNode::Dir(_, children) => {
|
||||
children.iter().any(|node| contains_node(node, selected_obj))
|
||||
}
|
||||
@@ -449,6 +469,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
|
||||
}
|
||||
|
||||
fn filter_node(
|
||||
units: &[ProjectObject],
|
||||
node: &ProjectObjectNode,
|
||||
search: &str,
|
||||
filter_diffable: bool,
|
||||
@@ -456,12 +477,12 @@ fn filter_node(
|
||||
show_hidden: bool,
|
||||
) -> Option<ProjectObjectNode> {
|
||||
match node {
|
||||
ProjectObjectNode::File(name, object) => {
|
||||
ProjectObjectNode::Unit(name, idx) => {
|
||||
let unit = &units[*idx];
|
||||
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
||||
&& (!filter_diffable
|
||||
|| (object.base_path.is_some() && object.target_path.is_some()))
|
||||
&& (!filter_incomplete || matches!(object.complete(), None | Some(false)))
|
||||
&& (show_hidden || !object.hidden())
|
||||
&& (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|
||||
&& (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
|
||||
&& (show_hidden || !unit.hidden())
|
||||
{
|
||||
Some(node.clone())
|
||||
} else {
|
||||
@@ -472,7 +493,14 @@ fn filter_node(
|
||||
let new_children = children
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden)
|
||||
filter_node(
|
||||
units,
|
||||
child,
|
||||
search,
|
||||
filter_diffable,
|
||||
filter_incomplete,
|
||||
show_hidden,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !new_children.is_empty() {
|
||||
@@ -530,33 +558,33 @@ fn pick_folder_ui(
|
||||
|
||||
pub fn project_window(
|
||||
ctx: &egui::Context,
|
||||
config: &AppConfigRef,
|
||||
state: &AppStateRef,
|
||||
show: &mut bool,
|
||||
state: &mut ConfigViewState,
|
||||
config_state: &mut ConfigViewState,
|
||||
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| {
|
||||
split_obj_config_ui(ui, &mut config_guard, state, appearance);
|
||||
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
||||
});
|
||||
|
||||
if let Some(error) = &state.load_error {
|
||||
if let Some(error) = &state_guard.config_error {
|
||||
let mut open = true;
|
||||
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
||||
ui.label("Failed to load project config:");
|
||||
ui.colored_label(appearance.delete_color, error);
|
||||
});
|
||||
if !open {
|
||||
state.load_error = None;
|
||||
state_guard.config_error = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_obj_config_ui(
|
||||
ui: &mut egui::Ui,
|
||||
config: &mut AppConfig,
|
||||
state: &mut ConfigViewState,
|
||||
state: &mut AppState,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
||||
@@ -567,7 +595,7 @@ fn split_obj_config_ui(
|
||||
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&config.project_dir,
|
||||
&state.config.project_dir,
|
||||
"Project directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
@@ -583,7 +611,7 @@ fn split_obj_config_ui(
|
||||
true,
|
||||
);
|
||||
if response.clicked() {
|
||||
state.file_dialog_state.queue(
|
||||
config_state.file_dialog_state.queue(
|
||||
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
||||
FileDialogResult::ProjectDir,
|
||||
);
|
||||
@@ -612,33 +640,35 @@ fn split_obj_config_ui(
|
||||
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
|
||||
.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
state.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.changed()
|
||||
{
|
||||
if custom_make_str.is_empty() {
|
||||
config.custom_make = None;
|
||||
state.config.custom_make = None;
|
||||
} else {
|
||||
config.custom_make = Some(custom_make_str);
|
||||
state.config.custom_make = Some(custom_make_str);
|
||||
}
|
||||
}
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
{
|
||||
if state.available_wsl_distros.is_none() {
|
||||
state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||
if config_state.available_wsl_distros.is_none() {
|
||||
config_state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||
}
|
||||
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| {
|
||||
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled");
|
||||
for distro in state.available_wsl_distros.as_ref().unwrap() {
|
||||
ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
|
||||
for distro in config_state.available_wsl_distros.as_ref().unwrap() {
|
||||
ui.selectable_value(
|
||||
&mut config.selected_wsl_distro,
|
||||
&mut state.config.selected_wsl_distro,
|
||||
Some(distro.clone()),
|
||||
distro,
|
||||
);
|
||||
@@ -647,10 +677,10 @@ fn split_obj_config_ui(
|
||||
}
|
||||
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(
|
||||
ui,
|
||||
&config.target_obj_dir,
|
||||
&state.config.target_obj_dir,
|
||||
"Target build directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
@@ -667,17 +697,17 @@ fn split_obj_config_ui(
|
||||
ui.label(job);
|
||||
},
|
||||
appearance,
|
||||
config.project_config_info.is_none(),
|
||||
state.project_config_info.is_none(),
|
||||
);
|
||||
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()),
|
||||
FileDialogResult::TargetDir,
|
||||
);
|
||||
}
|
||||
ui.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut config.build_target, "Build target objects"),
|
||||
state.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.on_hover_ui(|ui| {
|
||||
@@ -711,7 +741,7 @@ fn split_obj_config_ui(
|
||||
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&config.base_obj_dir,
|
||||
&state.config.base_obj_dir,
|
||||
"Base build directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
@@ -723,17 +753,17 @@ fn split_obj_config_ui(
|
||||
ui.label(job);
|
||||
},
|
||||
appearance,
|
||||
config.project_config_info.is_none(),
|
||||
state.project_config_info.is_none(),
|
||||
);
|
||||
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()),
|
||||
FileDialogResult::BaseDir,
|
||||
);
|
||||
}
|
||||
ui.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut config.build_base, "Build base objects"),
|
||||
state.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.on_hover_ui(|ui| {
|
||||
@@ -764,7 +794,7 @@ fn split_obj_config_ui(
|
||||
|
||||
subheading(ui, "Watch settings", appearance);
|
||||
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();
|
||||
job.append(
|
||||
"Automatically re-run the build & diff when files change.",
|
||||
@@ -774,23 +804,23 @@ fn split_obj_config_ui(
|
||||
ui.label(job);
|
||||
});
|
||||
if response.changed() {
|
||||
config.watcher_change = true;
|
||||
state.watcher_change = true;
|
||||
};
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
||||
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)
|
||||
.clicked()
|
||||
{
|
||||
config.watch_patterns =
|
||||
state.config.watch_patterns =
|
||||
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;
|
||||
for (idx, glob) in config.watch_patterns.iter().enumerate() {
|
||||
for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new(format!("{}", glob))
|
||||
@@ -798,7 +828,7 @@ fn split_obj_config_ui(
|
||||
.family(FontFamily::Monospace),
|
||||
);
|
||||
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)
|
||||
.clicked()
|
||||
{
|
||||
@@ -807,24 +837,24 @@ fn split_obj_config_ui(
|
||||
});
|
||||
}
|
||||
if let Some(idx) = remove_at {
|
||||
config.watch_patterns.remove(idx);
|
||||
config.watcher_change = true;
|
||||
state.config.watch_patterns.remove(idx);
|
||||
state.watcher_change = true;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0),
|
||||
state.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||
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)
|
||||
.clicked()
|
||||
{
|
||||
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
|
||||
config.watch_patterns.push(glob);
|
||||
config.watcher_change = true;
|
||||
state.watch_pattern_text.clear();
|
||||
if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
|
||||
state.config.watch_patterns.push(glob);
|
||||
state.watcher_change = true;
|
||||
config_state.watch_pattern_text.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -832,131 +862,131 @@ fn split_obj_config_ui(
|
||||
|
||||
pub fn arch_config_window(
|
||||
ctx: &egui::Context,
|
||||
config: &AppConfigRef,
|
||||
state: &AppStateRef,
|
||||
show: &mut bool,
|
||||
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| {
|
||||
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");
|
||||
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| {
|
||||
for &formatter in X86Formatter::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.x86_formatter == formatter,
|
||||
state.config.diff_obj_config.x86_formatter == formatter,
|
||||
formatter.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.x86_formatter = formatter;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.x86_formatter = formatter;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("MIPS");
|
||||
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| {
|
||||
for &abi in MipsAbi::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.mips_abi == abi,
|
||||
state.config.diff_obj_config.mips_abi == abi,
|
||||
abi.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.mips_abi = abi;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.mips_abi = abi;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
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| {
|
||||
for &category in MipsInstrCategory::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.mips_instr_category == category,
|
||||
state.config.diff_obj_config.mips_instr_category == category,
|
||||
category.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.mips_instr_category = category;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.mips_instr_category = category;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("ARM");
|
||||
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| {
|
||||
for &version in ArmArchVersion::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.arm_arch_version == version,
|
||||
state.config.diff_obj_config.arm_arch_version == version,
|
||||
version.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.arm_arch_version = version;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.arm_arch_version = version;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
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).");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
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");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
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| {
|
||||
for &usage in ArmR9Usage::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.arm_r9_usage == usage,
|
||||
state.config.diff_obj_config.arm_r9_usage == usage,
|
||||
usage.get_message().unwrap(),
|
||||
)
|
||||
.on_hover_text(usage.get_detailed_message().unwrap())
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.arm_r9_usage = usage;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.arm_r9_usage = usage;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
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.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
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.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
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.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{cmp::min, default::Default, mem::take};
|
||||
|
||||
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
|
||||
use objdiff_core::{
|
||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
|
||||
obj::ObjInfo,
|
||||
@@ -10,14 +9,15 @@ use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{DiffViewState, SymbolRefByName, View},
|
||||
column_layout::{render_header, render_table},
|
||||
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
|
||||
write_text,
|
||||
};
|
||||
|
||||
const BYTES_PER_ROW: usize = 16;
|
||||
|
||||
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> {
|
||||
obj.sections.iter().position(|section| section.name == selected_symbol.section_name)
|
||||
fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
|
||||
obj.sections.iter().position(|section| section.name == section_name)
|
||||
}
|
||||
|
||||
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
|
||||
@@ -131,20 +131,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
||||
split_diffs
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SectionDiffContext<'a> {
|
||||
obj: &'a ObjInfo,
|
||||
diff: &'a ObjDiff,
|
||||
section_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> SectionDiffContext<'a> {
|
||||
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
|
||||
obj.map(|(obj, diff)| Self {
|
||||
obj,
|
||||
diff,
|
||||
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_section(&self) -> bool { self.section_index.is_some() }
|
||||
}
|
||||
|
||||
fn data_table_ui(
|
||||
table: TableBuilder<'_>,
|
||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
left_ctx: Option<SectionDiffContext<'_>>,
|
||||
right_ctx: Option<SectionDiffContext<'_>>,
|
||||
config: &Appearance,
|
||||
) -> Option<()> {
|
||||
let left_section = left_obj.and_then(|(obj, diff)| {
|
||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
||||
});
|
||||
let right_section = right_obj.and_then(|(obj, diff)| {
|
||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
||||
});
|
||||
|
||||
let left_section = left_ctx
|
||||
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||
let right_section = right_ctx
|
||||
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||
let total_bytes = left_section
|
||||
.or(right_section)?
|
||||
.1
|
||||
@@ -159,118 +176,117 @@ fn data_table_ui(
|
||||
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
|
||||
table.body(|body| {
|
||||
body.rows(config.code_font.size, total_rows, |mut row| {
|
||||
let row_index = row.index();
|
||||
let address = row_index * BYTES_PER_ROW;
|
||||
row.col(|ui| {
|
||||
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
|
||||
let i = row.index();
|
||||
let address = i * BYTES_PER_ROW;
|
||||
row.col(|ui| {
|
||||
if column == 0 {
|
||||
if let Some(left_diffs) = &left_diffs {
|
||||
data_row_ui(ui, address, &left_diffs[row_index], config);
|
||||
data_row_ui(ui, address, &left_diffs[i], config);
|
||||
}
|
||||
});
|
||||
row.col(|ui| {
|
||||
} else if column == 1 {
|
||||
if let Some(right_diffs) = &right_diffs {
|
||||
data_row_ui(ui, address, &right_diffs[row_index], config);
|
||||
data_row_ui(ui, address, &right_diffs[i], config);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
#[must_use]
|
||||
pub fn data_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let section_name =
|
||||
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|
||||
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
|
||||
);
|
||||
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
|
||||
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if !right_ctx.map_or(false, |ctx| ctx.has_section())
|
||||
&& !left_ctx.map_or(false, |ctx| ctx.has_section())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
if let Some(section) =
|
||||
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||
{
|
||||
ui.label(
|
||||
RichText::new(section.name.clone())
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.label("");
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
if let Some(section) =
|
||||
right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||
{
|
||||
ui.label(
|
||||
RichText::new(section.name.clone())
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
let available_height = ui.available_height();
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(false)
|
||||
.cell_layout(Layout::left_to_right(Align::Min))
|
||||
.columns(Column::exact(column_width).clip(true), 2)
|
||||
.resizable(false)
|
||||
.auto_shrink([false, false])
|
||||
.min_scrolled_height(available_height);
|
||||
data_table_ui(
|
||||
table,
|
||||
result.first_obj.as_ref(),
|
||||
result.second_obj.as_ref(),
|
||||
selected_symbol,
|
||||
appearance,
|
||||
);
|
||||
let id =
|
||||
Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
|
||||
.with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
|
||||
ui.push_id(id, |ui| {
|
||||
data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
|
||||
});
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,42 +1,34 @@
|
||||
use egui::{Align, Layout, ScrollArea, Ui, Vec2};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use egui::{RichText, ScrollArea};
|
||||
use objdiff_core::{
|
||||
arch::ppc::ExceptionInfo,
|
||||
diff::ObjDiff,
|
||||
obj::{ObjInfo, ObjSymbol, SymbolRef},
|
||||
obj::{ObjInfo, ObjSymbol},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
||||
column_layout::{render_header, render_strips},
|
||||
function_diff::FunctionDiffContext,
|
||||
symbol_diff::{
|
||||
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
|
||||
View,
|
||||
},
|
||||
};
|
||||
|
||||
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == selected_symbol.symbol_name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_extab(extab: &ExceptionInfo) -> String {
|
||||
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 each function name, use the demangled name by default,
|
||||
//and if not available fallback to the original name
|
||||
let name = match &dtor.demangled_name {
|
||||
Some(demangled_name) => demangled_name,
|
||||
None => &dtor.name,
|
||||
let name: String = match &dtor.demangled_name {
|
||||
Some(demangled_name) => demangled_name.to_string(),
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -48,14 +40,12 @@ fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a Exce
|
||||
}
|
||||
|
||||
fn extab_text_ui(
|
||||
ui: &mut Ui,
|
||||
obj: &(ObjInfo, ObjDiff),
|
||||
symbol_ref: SymbolRef,
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
) -> Option<()> {
|
||||
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
|
||||
|
||||
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
|
||||
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
|
||||
let text = decode_extab(extab_entry);
|
||||
ui.colored_label(appearance.replace_color, &text);
|
||||
return Some(());
|
||||
@@ -65,137 +55,194 @@ fn extab_text_ui(
|
||||
}
|
||||
|
||||
fn extab_ui(
|
||||
ui: &mut Ui,
|
||||
obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
appearance: &Appearance,
|
||||
_left: bool,
|
||||
_column: usize,
|
||||
) {
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
|
||||
if let (Some(object), Some(symbol_ref)) = (obj, symbol) {
|
||||
extab_text_ui(ui, object, symbol_ref, appearance);
|
||||
if let Some((_section, symbol)) =
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
{
|
||||
extab_text_ui(ui, ctx, symbol, appearance);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
#[must_use]
|
||||
pub fn extab_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut left_ctx = FunctionDiffContext::new(
|
||||
result.first_obj.as_ref(),
|
||||
state.symbol_state.left_symbol.as_ref(),
|
||||
);
|
||||
let mut right_ctx = FunctionDiffContext::new(
|
||||
result.second_obj.as_ref(),
|
||||
state.symbol_state.right_symbol.as_ref(),
|
||||
);
|
||||
|
||||
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
});
|
||||
|
||||
let name = selected_symbol
|
||||
.demangled_symbol_name
|
||||
.as_deref()
|
||||
.unwrap_or(&selected_symbol.symbol_name);
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.colored_label(appearance.highlight_color, name);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
.on_disabled_hover_text("Scratch configuration missing")
|
||||
.clicked()
|
||||
{
|
||||
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
}) {
|
||||
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_section, symbol)) = left_ctx
|
||||
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||
{
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if let Some(match_percent) = result
|
||||
.second_obj
|
||||
.as_ref()
|
||||
.and_then(|(obj, diff)| {
|
||||
find_symbol(obj, selected_symbol).map(|sref| {
|
||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
||||
})
|
||||
})
|
||||
.and_then(|symbol| symbol.match_percent)
|
||||
{
|
||||
ui.colored_label(
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
format!("{match_percent:.0}%"),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
}
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| {
|
||||
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||
})
|
||||
}) {
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||
.font(appearance.code_font.clone())
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true);
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
}
|
||||
});
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,25 +1,78 @@
|
||||
use std::default::Default;
|
||||
|
||||
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget};
|
||||
use egui_extras::{Column, TableBuilder, TableRow};
|
||||
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
|
||||
use egui_extras::TableRow;
|
||||
use objdiff_core::{
|
||||
arch::ObjArch,
|
||||
diff::{
|
||||
display::{display_diff, DiffText, HighlightKind},
|
||||
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
||||
},
|
||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef},
|
||||
obj::{
|
||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
|
||||
SymbolRef,
|
||||
},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
||||
column_layout::{render_header, render_strips, render_table},
|
||||
symbol_diff::{
|
||||
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
|
||||
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FunctionViewState {
|
||||
pub highlight: HighlightKind,
|
||||
left_highlight: HighlightKind,
|
||||
right_highlight: HighlightKind,
|
||||
}
|
||||
|
||||
impl FunctionViewState {
|
||||
pub fn highlight(&self, column: usize) -> &HighlightKind {
|
||||
match column {
|
||||
0 => &self.left_highlight,
|
||||
1 => &self.right_highlight,
|
||||
_ => &HighlightKind::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
|
||||
match column {
|
||||
0 => {
|
||||
if highlight == self.left_highlight {
|
||||
if highlight == self.right_highlight {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
} else {
|
||||
self.right_highlight = self.left_highlight.clone();
|
||||
}
|
||||
} else {
|
||||
self.left_highlight = highlight;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if highlight == self.right_highlight {
|
||||
if highlight == self.left_highlight {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
} else {
|
||||
self.left_highlight = self.right_highlight.clone();
|
||||
}
|
||||
} else {
|
||||
self.right_highlight = highlight;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_highlight(&mut self) {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
}
|
||||
}
|
||||
|
||||
fn ins_hover_ui(
|
||||
@@ -79,6 +132,12 @@ fn ins_hover_ui(
|
||||
appearance.highlight_color,
|
||||
format!("Size: {:x}", reloc.target.size),
|
||||
);
|
||||
if let Some(s) = arch
|
||||
.guess_data_type(ins)
|
||||
.and_then(|ty| arch.display_data_type(ty, &reloc.target.bytes))
|
||||
{
|
||||
ui.colored_label(appearance.highlight_color, s);
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(appearance.highlight_color, "Extern".to_string());
|
||||
}
|
||||
@@ -167,15 +226,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn diff_text_ui(
|
||||
ui: &mut egui::Ui,
|
||||
text: DiffText<'_>,
|
||||
ins_diff: &ObjInsDiff,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
space_width: f32,
|
||||
response_cb: impl Fn(Response) -> Response,
|
||||
) {
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let label_text;
|
||||
let mut base_color = match ins_diff.kind {
|
||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||
@@ -229,7 +292,7 @@ fn diff_text_ui(
|
||||
}
|
||||
DiffText::Spacing(n) => {
|
||||
ui.add_space(n as f32 * space_width);
|
||||
return;
|
||||
return ret;
|
||||
}
|
||||
DiffText::Eol => {
|
||||
label_text = "\n".to_string();
|
||||
@@ -237,7 +300,7 @@ fn diff_text_ui(
|
||||
}
|
||||
|
||||
let len = label_text.len();
|
||||
let highlight = ins_view_state.highlight == text;
|
||||
let highlight = *ins_view_state.highlight(column) == text;
|
||||
let mut response = Label::new(LayoutJob::single_section(
|
||||
label_text,
|
||||
appearance.code_text_format(base_color, highlight),
|
||||
@@ -246,239 +309,511 @@ fn diff_text_ui(
|
||||
.ui(ui);
|
||||
response = response_cb(response);
|
||||
if response.clicked() {
|
||||
if highlight {
|
||||
ins_view_state.highlight = HighlightKind::None;
|
||||
} else {
|
||||
ins_view_state.highlight = text.into();
|
||||
}
|
||||
ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
|
||||
}
|
||||
if len < pad_to {
|
||||
ui.add_space((pad_to - len) as f32 * space_width);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_row_ui(
|
||||
ui: &mut egui::Ui,
|
||||
ins_diff: &ObjInsDiff,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
response_cb: impl Fn(Response) -> Response,
|
||||
) {
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
if ins_diff.kind != ObjInsDiffKind::None {
|
||||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||
}
|
||||
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
||||
display_diff(ins_diff, symbol.address, |text| {
|
||||
diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width, &response_cb);
|
||||
if let Some(action) = diff_text_ui(
|
||||
ui,
|
||||
text,
|
||||
ins_diff,
|
||||
appearance,
|
||||
ins_view_state,
|
||||
column,
|
||||
space_width,
|
||||
&response_cb,
|
||||
) {
|
||||
ret = Some(action);
|
||||
}
|
||||
Ok::<_, ()>(())
|
||||
})
|
||||
.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_col_ui(
|
||||
row: &mut TableRow<'_, '_>,
|
||||
obj: &(ObjInfo, ObjDiff),
|
||||
symbol_ref: SymbolRef,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
) {
|
||||
let (section, symbol) = obj.0.section_symbol(symbol_ref);
|
||||
let section = section.unwrap();
|
||||
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()];
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let symbol_ref = ctx.symbol_ref?;
|
||||
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
|
||||
let section = section?;
|
||||
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
|
||||
let response_cb = |response: Response| {
|
||||
if let Some(ins) = &ins_diff.ins {
|
||||
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
||||
response.on_hover_ui_at_pointer(|ui| {
|
||||
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance)
|
||||
ins_hover_ui(ui, ctx.obj.arch.as_ref(), section, ins, symbol, appearance)
|
||||
})
|
||||
} else {
|
||||
response
|
||||
}
|
||||
};
|
||||
let (_, response) = row.col(|ui| {
|
||||
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, response_cb);
|
||||
if let Some(action) =
|
||||
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
});
|
||||
response_cb(response);
|
||||
ret
|
||||
}
|
||||
|
||||
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
|
||||
row.col(|ui| {
|
||||
ui.label("");
|
||||
});
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_table_ui(
|
||||
table: TableBuilder<'_>,
|
||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
left_ctx: Option<FunctionDiffContext<'_>>,
|
||||
right_ctx: Option<FunctionDiffContext<'_>>,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
) -> Option<()> {
|
||||
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
let instructions_len = match (left_symbol, right_symbol) {
|
||||
(Some(left_symbol_ref), Some(right_symbol_ref)) => {
|
||||
let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len();
|
||||
let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len();
|
||||
debug_assert_eq!(left_len, right_len);
|
||||
ins_view_state: &FunctionViewState,
|
||||
symbol_state: &SymbolViewState,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let left_len = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||
});
|
||||
let right_len = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||
});
|
||||
let instructions_len = match (left_len, right_len) {
|
||||
(Some(left_len), Some(right_len)) => {
|
||||
if left_len != right_len {
|
||||
ui.label("Instruction count mismatch");
|
||||
return None;
|
||||
}
|
||||
left_len
|
||||
}
|
||||
(Some(left_symbol_ref), None) => {
|
||||
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len()
|
||||
(Some(left_len), None) => left_len,
|
||||
(None, Some(right_len)) => right_len,
|
||||
(None, None) => {
|
||||
ui.label("No symbol selected");
|
||||
return None;
|
||||
}
|
||||
(None, Some(right_symbol_ref)) => {
|
||||
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
|
||||
}
|
||||
(None, None) => return None,
|
||||
};
|
||||
table.body(|body| {
|
||||
body.rows(appearance.code_font.size, instructions_len, |mut row| {
|
||||
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) {
|
||||
asm_col_ui(&mut row, left_obj, left_symbol_ref, appearance, ins_view_state);
|
||||
} else {
|
||||
empty_col_ui(&mut row);
|
||||
}
|
||||
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) {
|
||||
asm_col_ui(&mut row, right_obj, right_symbol_ref, appearance, ins_view_state);
|
||||
} else {
|
||||
empty_col_ui(&mut row);
|
||||
if left_len.is_some() && right_len.is_some() {
|
||||
// Joint view
|
||||
render_table(
|
||||
ui,
|
||||
available_width,
|
||||
2,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Split view, one side is the symbol list
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
if ctx.has_symbol() {
|
||||
render_table(
|
||||
ui,
|
||||
available_width / 2.0,
|
||||
1,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if let Some((right_ctx, right_symbol_ref)) =
|
||||
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||
{
|
||||
if let Some(action) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||
None,
|
||||
symbol_state,
|
||||
SymbolFilter::Mapping(right_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
left_symbol: Some(left_symbol_ref),
|
||||
..
|
||||
}) => {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.obj.section_symbol(right_symbol_ref);
|
||||
ret = Some(DiffViewAction::SetMapping(
|
||||
match right_section.map(|s| s.kind) {
|
||||
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||
_ => View::SymbolDiff,
|
||||
},
|
||||
left_symbol_ref,
|
||||
SymbolRefByName::new(right_symbol, right_section),
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("No left object");
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
if ctx.has_symbol() {
|
||||
render_table(
|
||||
ui,
|
||||
available_width / 2.0,
|
||||
1,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if let Some((left_ctx, left_symbol_ref)) =
|
||||
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||
{
|
||||
if let Some(action) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||
None,
|
||||
symbol_state,
|
||||
SymbolFilter::Mapping(left_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
right_symbol: Some(right_symbol_ref),
|
||||
..
|
||||
}) => {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.obj.section_symbol(left_symbol_ref);
|
||||
ret = Some(DiffViewAction::SetMapping(
|
||||
match left_section.map(|s| s.kind) {
|
||||
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||
_ => View::SymbolDiff,
|
||||
},
|
||||
SymbolRefByName::new(left_symbol, left_section),
|
||||
right_symbol_ref,
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("No right object");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct FunctionDiffContext<'a> {
|
||||
pub obj: &'a ObjInfo,
|
||||
pub diff: &'a ObjDiff,
|
||||
pub symbol_ref: Option<SymbolRef>,
|
||||
}
|
||||
|
||||
impl<'a> FunctionDiffContext<'a> {
|
||||
pub fn new(
|
||||
obj: Option<&'a (ObjInfo, ObjDiff)>,
|
||||
selected_symbol: Option<&SymbolRefByName>,
|
||||
) -> Option<Self> {
|
||||
obj.map(|(obj, diff)| Self {
|
||||
obj,
|
||||
diff,
|
||||
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn function_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut left_ctx = FunctionDiffContext::new(
|
||||
result.first_obj.as_ref(),
|
||||
state.symbol_state.left_symbol.as_ref(),
|
||||
);
|
||||
let mut right_ctx = FunctionDiffContext::new(
|
||||
result.second_obj.as_ref(),
|
||||
state.symbol_state.right_symbol.as_ref(),
|
||||
);
|
||||
|
||||
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
.on_disabled_hover_text("Scratch configuration missing")
|
||||
.clicked()
|
||||
{
|
||||
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
}) {
|
||||
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
if let Some((_section, symbol)) = left_ctx
|
||||
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||
{
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if right_ctx.map_or(false, |m| m.has_symbol())
|
||||
&& ui
|
||||
.button("Change target")
|
||||
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
|
||||
.clicked()
|
||||
{
|
||||
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
|
||||
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose target symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| {
|
||||
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||
})
|
||||
}) {
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||
.font(appearance.code_font.clone())
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
if left_ctx.map_or(false, |m| m.has_symbol()) {
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running && state.scratch_available,
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
.button("Change base")
|
||||
.on_hover_text_at_pointer(
|
||||
"Choose a different symbol to use as the base",
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
.on_disabled_hover_text("Scratch configuration missing")
|
||||
.clicked()
|
||||
{
|
||||
state.queue_scratch = true;
|
||||
}
|
||||
});
|
||||
|
||||
let name = selected_symbol
|
||||
.demangled_symbol_name
|
||||
.as_deref()
|
||||
.unwrap_or(&selected_symbol.symbol_name);
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.colored_label(appearance.highlight_color, name);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
|
||||
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if let Some(match_percent) = result
|
||||
.second_obj
|
||||
.as_ref()
|
||||
.and_then(|(obj, diff)| {
|
||||
find_symbol(obj, selected_symbol).map(|sref| {
|
||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
||||
})
|
||||
})
|
||||
.and_then(|symbol| symbol.match_percent)
|
||||
{
|
||||
ui.colored_label(
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
format!("{match_percent:.0}%"),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
}
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose base symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
let available_height = ui.available_height();
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(false)
|
||||
.cell_layout(Layout::left_to_right(Align::Min))
|
||||
.columns(Column::exact(column_width).clip(true), 2)
|
||||
.resizable(false)
|
||||
.auto_shrink([false, false])
|
||||
.min_scrolled_height(available_height);
|
||||
asm_table_ui(
|
||||
table,
|
||||
result.first_obj.as_ref(),
|
||||
result.second_obj.as_ref(),
|
||||
selected_symbol,
|
||||
appearance,
|
||||
&mut state.function_state,
|
||||
);
|
||||
let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
|
||||
.with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
|
||||
if let Some(action) = ui
|
||||
.push_id(id, |ui| {
|
||||
asm_table_ui(
|
||||
ui,
|
||||
available_width,
|
||||
left_ctx,
|
||||
right_ctx,
|
||||
appearance,
|
||||
&state.function_state,
|
||||
&state.symbol_state,
|
||||
)
|
||||
})
|
||||
.inner
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -46,13 +47,13 @@ pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let file = File::open(path)?;
|
||||
let file = BufReader::new(File::open(path)?);
|
||||
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,58 +1,162 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
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) {
|
||||
ui.label("Jobs");
|
||||
if ui.button("Clear").clicked() {
|
||||
jobs.clear_errored();
|
||||
}
|
||||
|
||||
let mut remove_job: Option<usize> = None;
|
||||
let mut any_jobs = false;
|
||||
for job in jobs.iter_mut() {
|
||||
let Ok(status) = job.context.status.read() else {
|
||||
continue;
|
||||
};
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(&status.title);
|
||||
if ui.small_button("✖").clicked() {
|
||||
if job.handle.is_some() {
|
||||
job.should_remove = true;
|
||||
if let Err(e) = job.cancel.send(()) {
|
||||
log::error!("Failed to cancel job: {e:?}");
|
||||
}
|
||||
} else {
|
||||
remove_job = Some(job.id);
|
||||
any_jobs = true;
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(&status.title);
|
||||
if ui.small_button("✖").clicked() {
|
||||
if job.handle.is_some() {
|
||||
if let Err(e) = job.cancel.send(()) {
|
||||
log::error!("Failed to cancel job: {e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut bar = ProgressBar::new(status.progress_percent);
|
||||
if let Some(items) = &status.progress_items {
|
||||
bar = bar.text(format!("{} / {}", items[0], items[1]));
|
||||
}
|
||||
bar.ui(ui);
|
||||
const STATUS_LENGTH: usize = 80;
|
||||
if let Some(err) = &status.error {
|
||||
let err_string = format!("{:#}", err);
|
||||
ui.colored_label(
|
||||
appearance.delete_color,
|
||||
if err_string.len() > STATUS_LENGTH - 10 {
|
||||
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
||||
} else {
|
||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||
},
|
||||
)
|
||||
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
|
||||
} else {
|
||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||
} else {
|
||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||
})
|
||||
.on_hover_text_at_pointer(&status.status);
|
||||
remove_job = Some(job.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut bar = ProgressBar::new(status.progress_percent);
|
||||
if let Some(items) = &status.progress_items {
|
||||
bar = bar.text(format!("{} / {}", items[0], items[1]));
|
||||
}
|
||||
bar.ui(ui);
|
||||
const STATUS_LENGTH: usize = 80;
|
||||
if let Some(err) = &status.error {
|
||||
let err_string = format!("{:#}", err);
|
||||
ui.colored_label(
|
||||
appearance.delete_color,
|
||||
if err_string.len() > STATUS_LENGTH - 10 {
|
||||
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
||||
} else {
|
||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||
},
|
||||
)
|
||||
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
|
||||
.context_menu(|ui| {
|
||||
if ui.button("Copy full message").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = err_string);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||
} else {
|
||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||
})
|
||||
.on_hover_text_at_pointer(&status.status)
|
||||
.context_menu(|ui| {
|
||||
if ui.button("Copy full message").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = status.status.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if !any_jobs {
|
||||
ui.label("No jobs");
|
||||
}
|
||||
|
||||
if let Some(idx) = remove_job {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
||||
|
||||
pub(crate) mod appearance;
|
||||
pub(crate) mod column_layout;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod data_diff;
|
||||
pub(crate) mod debug;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user