diff --git a/Cargo.lock b/Cargo.lock index 85fca4a..a332571 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2457,7 +2457,7 @@ dependencies = [ [[package]] name = "objdiff" -version = "0.3.4" +version = "0.4.0" dependencies = [ "anyhow", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index d2d9794..5acaa16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "objdiff" -version = "0.3.4" +version = "0.4.0" edition = "2021" rust-version = "1.65" authors = ["Luke Street "] diff --git a/README.md b/README.md index dd4ef24..e0f972d 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,87 @@ A local diffing tool for decompilation projects. -Currently supports: +Supports: - PowerPC 750CL (GameCube & Wii) - MIPS (Nintendo 64) +See [Usage](#usage) for more information. + ![Symbol Screenshot](assets/screen-symbols.png) ![Diff Screenshot](assets/screen-diff.png) -### License +## Usage + +objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path from the "target" and "base" directories. + +For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual") object +is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used: + +- Target build directory: `build/asm` +- Base build directory: `build/src` +- Object: `MetroTRK/mslsupp.o` + +objdiff will then execute the build system from the project directory to build both objects: + +```sh +$ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled +$ make build/src/MetroTRK/mslsupp.o +``` + +The objects will then be compared and the results will be displayed in the UI. + +See [Configuration](#configuration) for more information. + +## Configuration + +While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or `objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in the root project directory. + +```json5 +// objdiff.json +{ + "custom_make": "ninja", + "target_dir": "build/mp1.0/asm", + "base_dir": "build/mp1.0/src", + "build_target": true, + "watch_patterns": [ + "*.c", + "*.cp", + "*.cpp", + "*.h", + "*.hpp", + "*.py" + ], + "objects": [ + { + "path": "MetroTRK/mslsupp.o", + "name": "MetroTRK/mslsupp", + "reverse_fn_order": false + }, + // ... + ] +} +``` + +- `custom_make` _(optional)_: By default, objdiff will use `make` to build the project. + If the project uses a different build system (e.g. `ninja`), specify it here. +- `target_dir`: Relative from the root of the project, this where the "target" or "expected" objects are located. + These are the **intended result** of the match. +- `base_dir`: Relative from the root of the project, this is where the "base" or "actual" objects are located. + These are objects built from the **current source code**. +- `build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`). + This is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files. + Requires the build system to be configured properly. +- `watch_patterns` _(optional)_: A list of glob patterns to watch for changes. ([Supported syntax](https://docs.rs/globset/latest/globset/#syntax)) + If any of these files change, objdiff will automatically rebuild the objects and re-compare them. +- `objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation. + - `path`: Relative path to the object from the `target_dir` and `base_dir`. + - `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used. + - `reverse_fn_order` _(optional)_: Displays function symbols in reversed order. + Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file. + + + +## License Licensed under either of diff --git a/assets/screen-diff.png b/assets/screen-diff.png index de775eb..8a412c6 100644 Binary files a/assets/screen-diff.png and b/assets/screen-diff.png differ diff --git a/assets/screen-symbols.png b/assets/screen-symbols.png index ecf3063..1ae1c13 100644 Binary files a/assets/screen-symbols.png and b/assets/screen-symbols.png differ diff --git a/src/app.rs b/src/app.rs index 47ca86f..a26ddd7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,10 +14,10 @@ use notify::{RecursiveMode, Watcher}; use time::UtcOffset; use crate::{ - config::{build_globset, load_project_config, ProjectUnit, ProjectUnitNode, CONFIG_FILENAMES}, - jobs::{ - check_update::start_check_update, objdiff::start_build, Job, JobQueue, JobResult, JobStatus, + config::{ + build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES, }, + jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus}, views::{ appearance::{appearance_window, Appearance}, config::{config_ui, project_window, ConfigViewState}, @@ -32,11 +32,11 @@ use crate::{ #[derive(Default)] pub struct ViewState { pub jobs: JobQueue, - pub show_appearance_config: bool, - pub demangle_state: DemangleViewState, - pub show_demangle: bool, - pub diff_state: DiffViewState, pub config_state: ConfigViewState, + pub demangle_state: DemangleViewState, + pub diff_state: DiffViewState, + pub show_appearance_config: bool, + pub show_demangle: bool, pub show_project_config: bool, } @@ -54,15 +54,17 @@ pub struct AppConfig { pub watch_patterns: Vec, #[serde(skip)] - pub units: Vec, + pub objects: Vec, #[serde(skip)] - pub unit_nodes: Vec, + pub object_nodes: Vec, #[serde(skip)] pub watcher_change: bool, #[serde(skip)] pub config_change: bool, #[serde(skip)] pub obj_change: bool, + #[serde(skip)] + pub queue_build: bool, } impl AppConfig { @@ -72,36 +74,42 @@ impl AppConfig { self.base_obj_dir = None; self.obj_path = None; self.build_target = false; - self.units.clear(); - self.unit_nodes.clear(); + self.objects.clear(); + self.object_nodes.clear(); self.watcher_change = true; self.config_change = true; self.obj_change = true; + self.queue_build = false; } pub fn set_target_obj_dir(&mut self, path: PathBuf) { self.target_obj_dir = Some(path); self.obj_path = None; self.obj_change = true; + self.queue_build = false; } pub fn set_base_obj_dir(&mut self, path: PathBuf) { self.base_obj_dir = Some(path); self.obj_path = None; self.obj_change = true; + self.queue_build = false; } pub fn set_obj_path(&mut self, path: String) { self.obj_path = Some(path); self.obj_change = true; + self.queue_build = false; } } +pub type AppConfigRef = Arc>; + #[derive(Default)] pub struct App { appearance: Appearance, view_state: ViewState, - config: Arc>, + config: AppConfigRef, modified: Arc, config_modified: Arc, watcher: Option, @@ -135,7 +143,7 @@ impl App { config.watcher_change = true; app.modified.store(true, Ordering::Relaxed); } - app.view_state.config_state.queue_update_check = config.auto_update_check; + app.view_state.config_state.queue_check_update = config.auto_update_check; app.config = Arc::new(RwLock::new(config)); } } @@ -147,6 +155,7 @@ impl App { fn pre_update(&mut self) { let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; + let mut results = vec![]; for (job, result) in jobs.iter_finished() { match result { Ok(result) => { @@ -157,18 +166,13 @@ impl App { log::error!("{:?}", err); } } - JobResult::ObjDiff(state) => { - diff_state.build = Some(state); - } - JobResult::CheckUpdate(state) => { - config_state.check_update = Some(state); - } JobResult::Update(state) => { if let Ok(mut guard) = self.relaunch_path.lock() { *guard = Some(state.exe_path); } self.should_relaunch = true; } + _ => results.push(result), } } Err(err) => { @@ -195,11 +199,19 @@ impl App { } } } + jobs.results.append(&mut results); jobs.clear_finished(); + + diff_state.pre_update(jobs, &self.config); + config_state.pre_update(jobs); + debug_assert!(jobs.results.is_empty()); } fn post_update(&mut self) { let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; + config_state.post_update(jobs, &self.config); + diff_state.post_update(jobs, &self.config); + let Ok(mut config) = self.config.write() else { return; }; @@ -244,22 +256,23 @@ impl App { } } - if config.obj_path.is_some() - && self.modified.swap(false, Ordering::Relaxed) - && !jobs.is_running(Job::ObjDiff) - { - jobs.push(start_build(self.config.clone())); - } - if config.obj_change { *diff_state = Default::default(); - jobs.push(start_build(self.config.clone())); + if config.obj_path.is_some() { + config.queue_build = true; + } config.obj_change = false; } - if config_state.queue_update_check { - jobs.push(start_check_update()); - config_state.queue_update_check = false; + if self.modified.swap(false, Ordering::Relaxed) { + config.queue_build = true; + } + + // 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.obj_path.is_some() && !jobs.is_running(Job::ObjDiff) { + jobs.push(start_build(self.config.clone())); + config.queue_build = false; } } } @@ -308,26 +321,19 @@ impl eframe::App for App { }); }); - if diff_state.current_view == View::FunctionDiff - && matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success) - { + 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| { - if function_diff_ui(ui, jobs, diff_state, appearance) { - jobs.push(start_build(config.clone())); - } + function_diff_ui(ui, diff_state, appearance); }); - } else if diff_state.current_view == View::DataDiff - && matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success) - { + } else if diff_state.current_view == View::DataDiff && build_success { egui::CentralPanel::default().show(ctx, |ui| { - if data_diff_ui(ui, jobs, diff_state, appearance) { - jobs.push(start_build(config.clone())); - } + data_diff_ui(ui, diff_state, appearance); }); } else { egui::SidePanel::left("side_panel").show(ctx, |ui| { egui::ScrollArea::both().show(ui, |ui| { - config_ui(ui, config, jobs, show_project_config, config_state, appearance); + config_ui(ui, config, show_project_config, config_state, appearance); jobs_ui(ui, jobs, appearance); }); }); diff --git a/src/config.rs b/src/config.rs index 356667d..226cf52 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,45 +16,48 @@ pub struct ProjectConfig { pub base_dir: Option, pub build_target: bool, pub watch_patterns: Vec, - pub units: Vec, + #[serde(alias = "units")] + pub objects: Vec, } #[derive(Default, Clone, serde::Deserialize)] -pub struct ProjectUnit { - pub name: String, +pub struct ProjectObject { + pub name: Option, pub path: PathBuf, - #[serde(default)] - pub reverse_fn_order: bool, + pub reverse_fn_order: Option, } #[derive(Clone)] -pub enum ProjectUnitNode { - File(String, ProjectUnit), - Dir(String, Vec), +pub enum ProjectObjectNode { + File(String, ProjectObject), + Dir(String, Vec), } -fn find_dir<'a>(name: &str, nodes: &'a mut Vec) -> &'a mut Vec { +fn find_dir<'a>( + name: &str, + nodes: &'a mut Vec, +) -> &'a mut Vec { if let Some(index) = nodes .iter() - .position(|node| matches!(node, ProjectUnitNode::Dir(dir_name, _) if dir_name == name)) + .position(|node| matches!(node, ProjectObjectNode::Dir(dir_name, _) if dir_name == name)) { - if let ProjectUnitNode::Dir(_, children) = &mut nodes[index] { + if let ProjectObjectNode::Dir(_, children) = &mut nodes[index] { return children; } } else { - nodes.push(ProjectUnitNode::Dir(name.to_string(), vec![])); - if let Some(ProjectUnitNode::Dir(_, children)) = nodes.last_mut() { + nodes.push(ProjectObjectNode::Dir(name.to_string(), vec![])); + if let Some(ProjectObjectNode::Dir(_, children)) = nodes.last_mut() { return children; } } unreachable!(); } -fn build_nodes(units: &[ProjectUnit]) -> Vec { +fn build_nodes(objects: &[ProjectObject]) -> Vec { let mut nodes = vec![]; - for unit in units { + for object in objects { let mut out_nodes = &mut nodes; - let path = Path::new(&unit.name); + let path = object.name.as_ref().map(Path::new).unwrap_or(&object.path); if let Some(parent) = path.parent() { for component in parent.components() { if let Component::Normal(name) = component { @@ -64,7 +67,7 @@ fn build_nodes(units: &[ProjectUnit]) -> Vec { } } let filename = path.file_name().unwrap().to_str().unwrap().to_string(); - out_nodes.push(ProjectUnitNode::File(filename, unit.clone())); + out_nodes.push(ProjectObjectNode::File(filename, object.clone())); } nodes } @@ -83,8 +86,8 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> { config.build_target = project_config.build_target; config.watch_patterns = project_config.watch_patterns; config.watcher_change = true; - config.units = project_config.units; - config.unit_nodes = build_nodes(&config.units); + config.objects = project_config.objects; + config.object_nodes = build_nodes(&config.objects); } Ok(()) } diff --git a/src/jobs/check_update.rs b/src/jobs/check_update.rs index 3b14e8a..2b29b2b 100644 --- a/src/jobs/check_update.rs +++ b/src/jobs/check_update.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use self_update::{cargo_crate_version, update::Release}; use crate::{ - jobs::{start_job, update_status, Job, JobResult, JobState, Status}, + jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef}, update::{build_updater, BIN_NAME}, }; @@ -14,7 +14,7 @@ pub struct CheckUpdateResult { pub found_binary: bool, } -fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result> { +fn run_check_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result> { update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?; let updater = build_updater().context("Failed to create release updater")?; let latest_release = updater.get_latest_release()?; @@ -28,6 +28,6 @@ fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result JobState { start_job("Check for updates", Job::CheckUpdate, move |status, cancel| { - run_check_update(status, cancel).map(JobResult::CheckUpdate) + run_check_update(status, cancel).map(|result| JobResult::CheckUpdate(Some(result))) }) } diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index 27d05e5..654e207 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -26,12 +26,22 @@ pub static JOB_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Default)] pub struct JobQueue { pub jobs: Vec, + pub results: Vec, } impl JobQueue { /// Adds a job to the queue. + #[inline] pub fn push(&mut self, state: JobState) { self.jobs.push(state); } + /// Adds a job to the queue if a job of the given kind is not already running. + #[inline] + pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) { + if !self.is_running(job) { + self.push(func()); + } + } + /// Returns whether a job of the given kind is running. pub fn is_running(&self, kind: Job) -> bool { self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some()) @@ -79,11 +89,13 @@ impl JobQueue { pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); } } +pub type JobStatusRef = Arc>; + pub struct JobState { pub id: usize, pub kind: Job, pub handle: Option>, - pub status: Arc>, + pub status: JobStatusRef, pub cancel: Sender<()>, pub should_remove: bool, } @@ -99,8 +111,8 @@ pub struct JobStatus { pub enum JobResult { None, - ObjDiff(Box), - CheckUpdate(Box), + ObjDiff(Option>), + CheckUpdate(Option>), Update(Box), } @@ -111,12 +123,10 @@ fn should_cancel(rx: &Receiver<()>) -> bool { } } -type Status = Arc>; - fn start_job( title: &str, kind: Job, - run: impl FnOnce(&Status, Receiver<()>) -> Result + Send + 'static, + run: impl FnOnce(&JobStatusRef, Receiver<()>) -> Result + Send + 'static, ) -> JobState { let status = Arc::new(RwLock::new(JobStatus { title: title.to_string(), @@ -151,7 +161,7 @@ fn start_job( } fn update_status( - status: &Status, + status: &JobStatusRef, str: String, count: u32, total: u32, diff --git a/src/jobs/objdiff.rs b/src/jobs/objdiff.rs index 6546b82..2f12503 100644 --- a/src/jobs/objdiff.rs +++ b/src/jobs/objdiff.rs @@ -1,17 +1,12 @@ -use std::{ - path::Path, - process::Command, - str::from_utf8, - sync::{mpsc::Receiver, Arc, RwLock}, -}; +use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver}; use anyhow::{Context, Error, Result}; use time::OffsetDateTime; use crate::{ - app::AppConfig, + app::{AppConfig, AppConfigRef}, diff::diff_objs, - jobs::{start_job, update_status, Job, JobResult, JobState, Status}, + jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef}, obj::{elf, ObjInfo}, }; @@ -76,9 +71,9 @@ fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus { } fn run_build( - status: &Status, + status: &JobStatusRef, cancel: Receiver<()>, - config: Arc>, + config: AppConfigRef, ) -> Result> { let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone(); let obj_path = config.obj_path.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; @@ -135,8 +130,8 @@ fn run_build( Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time })) } -pub fn start_build(config: Arc>) -> JobState { +pub fn start_build(config: AppConfigRef) -> JobState { start_job("Object diff", Job::ObjDiff, move |status, cancel| { - run_build(status, cancel, config).map(JobResult::ObjDiff) + run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) }) } diff --git a/src/jobs/update.rs b/src/jobs/update.rs index a9ae59c..3c76c17 100644 --- a/src/jobs/update.rs +++ b/src/jobs/update.rs @@ -9,7 +9,7 @@ use anyhow::{Context, Result}; use const_format::formatcp; use crate::{ - jobs::{start_job, update_status, Job, JobResult, JobState, Status}, + jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef}, update::{build_updater, BIN_NAME}, }; @@ -17,7 +17,7 @@ pub struct UpdateResult { pub exe_path: PathBuf, } -fn run_update(status: &Status, cancel: Receiver<()>) -> Result> { +fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result> { update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?; let updater = build_updater().context("Failed to create release updater")?; let latest_release = updater.get_latest_release()?; diff --git a/src/views/appearance.rs b/src/views/appearance.rs index 56c7fa4..cf12d6c 100644 --- a/src/views/appearance.rs +++ b/src/views/appearance.rs @@ -7,7 +7,6 @@ pub struct Appearance { pub ui_font: FontId, pub code_font: FontId, pub diff_colors: Vec, - pub reverse_fn_order: bool, pub theme: eframe::Theme, // Applied by theme @@ -37,7 +36,6 @@ impl Default for Appearance { ui_font: FontId { size: 12.0, family: FontFamily::Proportional }, code_font: FontId { size: 14.0, family: FontFamily::Monospace }, diff_colors: DEFAULT_COLOR_ROTATION.to_vec(), - reverse_fn_order: false, theme: eframe::Theme::Dark, text_color: Color32::GRAY, emphasized_text_color: Color32::LIGHT_GRAY, diff --git a/src/views/config.rs b/src/views/config.rs index c5e9c8b..aa18883 100644 --- a/src/views/config.rs +++ b/src/views/config.rs @@ -2,8 +2,8 @@ use std::string::FromUtf16Error; use std::{ borrow::Cow, + mem::take, path::{PathBuf, MAIN_SEPARATOR}, - sync::{Arc, RwLock}, }; #[cfg(windows)] @@ -17,9 +17,13 @@ use globset::Glob; use self_update::cargo_crate_version; use crate::{ - app::AppConfig, - config::{ProjectUnit, ProjectUnitNode}, - jobs::{check_update::CheckUpdateResult, objdiff::start_build, update::start_update, JobQueue}, + app::{AppConfig, AppConfigRef}, + config::{ProjectObject, ProjectObjectNode}, + jobs::{ + check_update::{start_check_update, CheckUpdateResult}, + update::start_update, + Job, JobQueue, JobResult, + }, update::RELEASE_URL, views::appearance::Appearance, }; @@ -27,14 +31,54 @@ use crate::{ #[derive(Default)] pub struct ConfigViewState { pub check_update: Option>, + pub check_update_running: bool, + pub queue_check_update: bool, + pub update_running: bool, + pub queue_update: bool, + pub build_running: bool, + pub queue_build: bool, pub watch_pattern_text: String, - pub queue_update_check: bool, pub load_error: Option, - pub unit_search: String, + pub object_search: String, #[cfg(windows)] pub available_wsl_distros: Option>, } +impl ConfigViewState { + pub fn pre_update(&mut self, jobs: &mut JobQueue) { + jobs.results.retain_mut(|result| { + if let JobResult::CheckUpdate(result) = result { + self.check_update = take(result); + false + } else { + true + } + }); + self.build_running = jobs.is_running(Job::ObjDiff); + self.check_update_running = jobs.is_running(Job::CheckUpdate); + self.update_running = jobs.is_running(Job::Update); + } + + pub fn post_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) { + if self.queue_build { + self.queue_build = false; + if let Ok(mut config) = config.write() { + config.queue_build = true; + } + } + + if self.queue_check_update { + self.queue_check_update = false; + jobs.push_once(Job::CheckUpdate, start_check_update); + } + + if self.queue_update { + self.queue_update = false; + jobs.push_once(Job::Update, start_update); + } + } +} + const DEFAULT_WATCH_PATTERNS: &[&str] = &[ "*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", "*.inc", "*.py", "*.yml", "*.txt", "*.json", @@ -75,8 +119,7 @@ fn fetch_wsl2_distros() -> Vec { pub fn config_ui( ui: &mut egui::Ui, - config: &Arc>, - jobs: &mut JobQueue, + config: &AppConfigRef, show_config_window: &mut bool, state: &mut ConfigViewState, appearance: &Appearance, @@ -88,15 +131,15 @@ pub fn config_ui( base_obj_dir, obj_path, auto_update_check, - units, - unit_nodes, + objects, + object_nodes, .. } = &mut *config_guard; ui.heading("Updates"); ui.checkbox(auto_update_check, "Check for updates on startup"); - if ui.button("Check now").clicked() { - state.queue_update_check = true; + if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() { + 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"))); @@ -104,20 +147,20 @@ pub fn config_ui( ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE"))); ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG"))); }); - if let Some(state) = &state.check_update { - ui.label(format!("Latest version: {}", state.latest_release.version)); - if state.update_available { + if let Some(result) = &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 state.found_binary + if result.found_binary && ui - .button("Automatic") + .add_enabled(!state.update_running, egui::Button::new("Automatic")) .on_hover_text_at_pointer( "Automatically download and replace the current build", ) .clicked() { - jobs.push(start_update()); + state.queue_update = true; } if ui .button("Manual") @@ -164,7 +207,7 @@ pub fn config_ui( if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { let mut new_build_obj = obj_path.clone(); - if units.is_empty() { + if objects.is_empty() { if ui.button("Select object").clicked() { if let Some(path) = rfd::FileDialog::new() .set_directory(&target_dir) @@ -186,8 +229,8 @@ pub fn config_ui( ); } } else { - let had_search = !state.unit_search.is_empty(); - egui::TextEdit::singleline(&mut state.unit_search).hint_text("Filter").ui(ui); + let had_search = !state.object_search.is_empty(); + egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui); let mut root_open = None; let mut node_open = NodeOpen::Default; @@ -209,7 +252,7 @@ pub fn config_ui( node_open = NodeOpen::Object; } }); - if state.unit_search.is_empty() { + if state.object_search.is_empty() { if had_search { root_open = Some(true); node_open = NodeOpen::Object; @@ -226,11 +269,11 @@ pub fn config_ui( .open(root_open) .default_open(true) .show(ui, |ui| { - let mut nodes = Cow::Borrowed(unit_nodes); - if !state.unit_search.is_empty() { - let search = state.unit_search.to_ascii_lowercase(); + let mut nodes = Cow::Borrowed(object_nodes); + if !state.object_search.is_empty() { + let search = state.object_search.to_ascii_lowercase(); nodes = Cow::Owned( - unit_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(), + object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(), ); } @@ -245,30 +288,30 @@ pub fn config_ui( if let Some(obj) = new_build_obj { // Will set obj_changed, which will trigger a rebuild config_guard.set_obj_path(obj); - // TODO apply reverse_fn_order } } - if config_guard.obj_path.is_some() && ui.button("Build").clicked() { - // Rebuild immediately - jobs.push(start_build(config.clone())); + if config_guard.obj_path.is_some() + && ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() + { + state.queue_build = true; } } else { ui.colored_label(appearance.delete_color, "Missing project settings"); } - // ui.checkbox(&mut view_config.reverse_fn_order, "Reverse function order (deferred)"); ui.separator(); } -fn display_unit( +fn display_object( ui: &mut egui::Ui, obj_path: &mut Option, name: &str, - unit: &ProjectUnit, + object: &ProjectObject, appearance: &Appearance, ) { - let path_string = unit.path.to_string_lossy().to_string(); + let path_string = object.path.to_string_lossy().to_string(); let selected = matches!(obj_path, Some(path) if path == &path_string); + let color = if selected { appearance.emphasized_text_color } else { appearance.text_color }; if SelectableLabel::new( selected, RichText::new(name) @@ -276,7 +319,7 @@ fn display_unit( size: appearance.ui_font.size, family: appearance.code_font.family.clone(), }) - .color(appearance.text_color), + .color(color), ) .ui(ui) .clicked() @@ -297,15 +340,15 @@ enum NodeOpen { fn display_node( ui: &mut egui::Ui, obj_path: &mut Option, - node: &ProjectUnitNode, + node: &ProjectObjectNode, appearance: &Appearance, node_open: NodeOpen, ) { match node { - ProjectUnitNode::File(name, unit) => { - display_unit(ui, obj_path, name, unit, appearance); + ProjectObjectNode::File(name, object) => { + display_object(ui, obj_path, name, object, appearance); } - ProjectUnitNode::Dir(name, children) => { + ProjectObjectNode::Dir(name, children) => { let contains_obj = obj_path.as_ref().map(|path| contains_node(node, path)); let open = match node_open { NodeOpen::Default => None, @@ -336,33 +379,35 @@ fn display_node( } } -fn contains_node(node: &ProjectUnitNode, path: &str) -> bool { +fn contains_node(node: &ProjectObjectNode, path: &str) -> bool { match node { - ProjectUnitNode::File(_, unit) => { - let path_string = unit.path.to_string_lossy().to_string(); + ProjectObjectNode::File(_, object) => { + let path_string = object.path.to_string_lossy().to_string(); path == path_string } - ProjectUnitNode::Dir(_, children) => children.iter().any(|node| contains_node(node, path)), + ProjectObjectNode::Dir(_, children) => { + children.iter().any(|node| contains_node(node, path)) + } } } -fn filter_node(node: &ProjectUnitNode, search: &str) -> Option { +fn filter_node(node: &ProjectObjectNode, search: &str) -> Option { match node { - ProjectUnitNode::File(name, _) => { + ProjectObjectNode::File(name, _) => { if name.to_ascii_lowercase().contains(search) { Some(node.clone()) } else { None } } - ProjectUnitNode::Dir(name, children) => { + ProjectObjectNode::Dir(name, children) => { if name.to_ascii_lowercase().contains(search) { return Some(node.clone()); } let new_children = children.iter().filter_map(|child| filter_node(child, search)).collect::>(); if !new_children.is_empty() { - Some(ProjectUnitNode::Dir(name.clone(), new_children)) + Some(ProjectObjectNode::Dir(name.clone(), new_children)) } else { None } @@ -411,7 +456,7 @@ fn pick_folder_ui( pub fn project_window( ctx: &egui::Context, - config: &Arc>, + config: &AppConfigRef, show: &mut bool, state: &mut ConfigViewState, appearance: &Appearance, diff --git a/src/views/data_diff.rs b/src/views/data_diff.rs index 5301d33..2b2adb8 100644 --- a/src/views/data_diff.rs +++ b/src/views/data_diff.rs @@ -5,7 +5,6 @@ use egui_extras::{Column, TableBuilder}; use time::format_description; use crate::{ - jobs::{Job, JobQueue}, obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection}, views::{ appearance::Appearance, @@ -164,15 +163,10 @@ fn data_table_ui( Some(()) } -pub fn data_diff_ui( - ui: &mut egui::Ui, - jobs: &JobQueue, - state: &mut DiffViewState, - appearance: &Appearance, -) -> bool { - let mut rebuild = false; - let (Some(result), Some(selected_symbol)) = (&state.build, &state.selected_symbol) else { - return rebuild; +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; }; // Header @@ -210,13 +204,16 @@ pub fn data_diff_ui( ui.set_width(column_width); ui.horizontal(|ui| { - if ui.button("Build").clicked() { - rebuild = true; + 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); ui.style_mut().wrap = Some(false); - if jobs.is_running(Job::ObjDiff) { + if state.build_running { ui.colored_label(appearance.replace_color, "Building…"); } else { ui.label("Last built:"); @@ -257,6 +254,4 @@ pub fn data_diff_ui( .min_scrolled_height(available_height); data_table_ui(table, left_obj, right_obj, selected_symbol, appearance); } - - rebuild } diff --git a/src/views/function_diff.rs b/src/views/function_diff.rs index 259bcf7..ec92731 100644 --- a/src/views/function_diff.rs +++ b/src/views/function_diff.rs @@ -8,7 +8,6 @@ use ppc750cl::Argument; use time::format_description; use crate::{ - jobs::{Job, JobQueue}, obj::{ ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc, ObjRelocKind, ObjSymbol, @@ -403,15 +402,10 @@ fn asm_table_ui( Some(()) } -pub fn function_diff_ui( - ui: &mut egui::Ui, - jobs: &JobQueue, - state: &mut DiffViewState, - appearance: &Appearance, -) -> bool { - let mut rebuild = false; - let (Some(result), Some(selected_symbol)) = (&state.build, &state.selected_symbol) else { - return rebuild; +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; }; // Header @@ -459,13 +453,16 @@ pub fn function_diff_ui( ui.set_width(column_width); ui.horizontal(|ui| { - if ui.button("Build").clicked() { - rebuild = true; + 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); ui.style_mut().wrap = Some(false); - if jobs.is_running(Job::ObjDiff) { + if state.build_running { ui.colored_label(appearance.replace_color, "Building…"); } else { ui.label("Last built:"); @@ -517,5 +514,4 @@ pub fn function_diff_ui( .min_scrolled_height(available_height); asm_table_ui(table, left_obj, right_obj, selected_symbol, appearance); } - rebuild } diff --git a/src/views/symbol_diff.rs b/src/views/symbol_diff.rs index 539c1ee..43dbd84 100644 --- a/src/views/symbol_diff.rs +++ b/src/views/symbol_diff.rs @@ -1,3 +1,5 @@ +use std::mem::take; + use egui::{ text::LayoutJob, Align, CollapsingHeader, Color32, Layout, Rgba, ScrollArea, SelectableLabel, TextEdit, Ui, Vec2, Widget, @@ -5,7 +7,11 @@ use egui::{ use egui_extras::{Size, StripBuilder}; use crate::{ - jobs::objdiff::{BuildStatus, ObjDiffResult}, + app::AppConfigRef, + jobs::{ + objdiff::{BuildStatus, ObjDiffResult}, + Job, JobQueue, JobResult, + }, obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags}, views::{appearance::Appearance, write_text}, }; @@ -16,7 +22,7 @@ pub struct SymbolReference { } #[allow(clippy::enum_variant_names)] -#[derive(Default, Eq, PartialEq)] +#[derive(Default, Eq, PartialEq, Copy, Clone)] pub enum View { #[default] SymbolDiff, @@ -28,9 +34,56 @@ pub enum View { pub struct DiffViewState { pub build: Option>, pub current_view: View, + pub symbol_state: SymbolViewState, + pub search: String, + pub queue_build: bool, + pub build_running: bool, +} + +#[derive(Default)] +pub struct SymbolViewState { pub highlighted_symbol: Option, pub selected_symbol: Option, - pub search: String, + pub reverse_fn_order: bool, + pub disable_reverse_fn_order: bool, +} + +impl DiffViewState { + pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) { + jobs.results.retain_mut(|result| { + if let JobResult::ObjDiff(result) = result { + self.build = take(result); + false + } else { + true + } + }); + self.build_running = jobs.is_running(Job::ObjDiff); + + self.symbol_state.disable_reverse_fn_order = false; + if let Ok(config) = config.read() { + if let Some(obj_path) = &config.obj_path { + if let Some(object) = config.objects.iter().find(|object| { + let path_string = object.path.to_string_lossy().to_string(); + &path_string == obj_path + }) { + if let Some(value) = object.reverse_fn_order { + self.symbol_state.reverse_fn_order = value; + self.symbol_state.disable_reverse_fn_order = true; + } + } + } + } + } + + pub fn post_update(&mut self, _jobs: &mut JobQueue, config: &AppConfigRef) { + if self.queue_build { + self.queue_build = false; + if let Ok(mut config) = config.write() { + config.queue_build = true; + } + } + } } pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Color32 { @@ -79,30 +132,25 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) { }); } +#[must_use] fn symbol_ui( ui: &mut Ui, symbol: &ObjSymbol, section: Option<&ObjSection>, - highlighted_symbol: &mut Option, - selected_symbol: &mut Option, - current_view: &mut View, + state: &mut SymbolViewState, appearance: &Appearance, -) { +) -> Option { + let mut ret = None; let mut job = LayoutJob::default(); let name: &str = if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name }; let mut selected = false; - if let Some(sym) = highlighted_symbol { + if let Some(sym) = &state.highlighted_symbol { selected = sym == &symbol.name; } write_text("[", appearance.text_color, &mut job, appearance.code_font.clone()); if symbol.flags.0.contains(ObjSymbolFlags::Common) { - write_text( - "c", - appearance.replace_color, /* Color32::from_rgb(0, 255, 255) */ - &mut job, - appearance.code_font.clone(), - ); + write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone()); } else if symbol.flags.0.contains(ObjSymbolFlags::Global) { write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone()); } else if symbol.flags.0.contains(ObjSymbolFlags::Local) { @@ -130,22 +178,23 @@ fn symbol_ui( if response.clicked() { if let Some(section) = section { if section.kind == ObjSectionKind::Code { - *selected_symbol = Some(SymbolReference { + state.selected_symbol = Some(SymbolReference { symbol_name: symbol.name.clone(), section_name: section.name.clone(), }); - *current_view = View::FunctionDiff; + ret = Some(View::FunctionDiff); } else if section.kind == ObjSectionKind::Data { - *selected_symbol = Some(SymbolReference { + state.selected_symbol = Some(SymbolReference { symbol_name: section.name.clone(), section_name: section.name.clone(), }); - *current_view = View::DataDiff; + ret = Some(View::DataDiff); } } } else if response.hovered() { - *highlighted_symbol = Some(symbol.name.clone()); + state.highlighted_symbol = Some(symbol.name.clone()); } + ret } fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool { @@ -158,16 +207,15 @@ fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool { .unwrap_or(false) } -#[allow(clippy::too_many_arguments)] +#[must_use] fn symbol_list_ui( ui: &mut Ui, obj: &ObjInfo, - highlighted_symbol: &mut Option, - selected_symbol: &mut Option, - current_view: &mut View, + state: &mut SymbolViewState, lower_search: &str, appearance: &Appearance, -) { +) -> Option { + let mut ret = None; ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); @@ -176,15 +224,7 @@ fn symbol_list_ui( if !obj.common.is_empty() { CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| { for symbol in &obj.common { - symbol_ui( - ui, - symbol, - None, - highlighted_symbol, - selected_symbol, - current_view, - appearance, - ); + ret = ret.or(symbol_ui(ui, symbol, None, state, appearance)); } }); } @@ -193,41 +233,28 @@ fn symbol_list_ui( CollapsingHeader::new(format!("{} ({:x})", section.name, section.size)) .default_open(true) .show(ui, |ui| { - if section.kind == ObjSectionKind::Code && appearance.reverse_fn_order { + if section.kind == ObjSectionKind::Code && state.reverse_fn_order { for symbol in section.symbols.iter().rev() { if !symbol_matches_search(symbol, lower_search) { continue; } - symbol_ui( - ui, - symbol, - Some(section), - highlighted_symbol, - selected_symbol, - current_view, - appearance, - ); + ret = + ret.or(symbol_ui(ui, symbol, Some(section), state, appearance)); } } else { for symbol in §ion.symbols { if !symbol_matches_search(symbol, lower_search) { continue; } - symbol_ui( - ui, - symbol, - Some(section), - highlighted_symbol, - selected_symbol, - current_view, - appearance, - ); + ret = + ret.or(symbol_ui(ui, symbol, Some(section), state, appearance)); } } }); } }); }); + ret } fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) { @@ -242,7 +269,7 @@ fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) { } pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) { - let DiffViewState { build, current_view, highlighted_symbol, selected_symbol, search } = state; + let DiffViewState { build, current_view, symbol_state, search, .. } = state; let Some(result) = build else { return; }; @@ -295,6 +322,14 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail"); } }); + + ui.add_enabled( + !symbol_state.disable_reverse_fn_order, + egui::Checkbox::new( + &mut symbol_state.reverse_fn_order, + "Reverse function order (-inline deferred)", + ), + ); }, ); }, @@ -302,6 +337,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea ui.separator(); // Table + let mut ret = None; let lower_search = search.to_ascii_lowercase(); StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { strip.strip(|builder| { @@ -310,15 +346,13 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea ui.push_id("left", |ui| { if result.first_status.success { if let Some(obj) = &result.first_obj { - symbol_list_ui( + ret = ret.or(symbol_list_ui( ui, obj, - highlighted_symbol, - selected_symbol, - current_view, + symbol_state, &lower_search, appearance, - ); + )); } } else { build_log_ui(ui, &result.first_status, appearance); @@ -329,15 +363,13 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea ui.push_id("right", |ui| { if result.second_status.success { if let Some(obj) = &result.second_obj { - symbol_list_ui( + ret = ret.or(symbol_list_ui( ui, obj, - highlighted_symbol, - selected_symbol, - current_view, + symbol_state, &lower_search, appearance, - ); + )); } } else { build_log_ui(ui, &result.second_status, appearance); @@ -347,4 +379,8 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea }); }); }); + + if let Some(view) = ret { + *current_view = view; + } }