Document configuration file & more cleanup

This commit is contained in:
Luke Street 2023-08-12 14:18:09 -04:00
parent eaf0fabc2d
commit 3970bc8acf
16 changed files with 389 additions and 232 deletions

2
Cargo.lock generated
View File

@ -2457,7 +2457,7 @@ dependencies = [
[[package]]
name = "objdiff"
version = "0.3.4"
version = "0.4.0"
dependencies = [
"anyhow",
"byteorder",

View File

@ -1,6 +1,6 @@
[package]
name = "objdiff"
version = "0.3.4"
version = "0.4.0"
edition = "2021"
rust-version = "1.65"
authors = ["Luke Street <luke@street.dev>"]

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -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<Glob>,
#[serde(skip)]
pub units: Vec<ProjectUnit>,
pub objects: Vec<ProjectObject>,
#[serde(skip)]
pub unit_nodes: Vec<ProjectUnitNode>,
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,
}
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<RwLock<AppConfig>>;
#[derive(Default)]
pub struct App {
appearance: Appearance,
view_state: ViewState,
config: Arc<RwLock<AppConfig>>,
config: AppConfigRef,
modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>,
@ -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);
});
});

View File

@ -16,45 +16,48 @@ pub struct ProjectConfig {
pub base_dir: Option<PathBuf>,
pub build_target: bool,
pub watch_patterns: Vec<Glob>,
pub units: Vec<ProjectUnit>,
#[serde(alias = "units")]
pub objects: Vec<ProjectObject>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectUnit {
pub name: String,
pub struct ProjectObject {
pub name: Option<String>,
pub path: PathBuf,
#[serde(default)]
pub reverse_fn_order: bool,
pub reverse_fn_order: Option<bool>,
}
#[derive(Clone)]
pub enum ProjectUnitNode {
File(String, ProjectUnit),
Dir(String, Vec<ProjectUnitNode>),
pub enum ProjectObjectNode {
File(String, ProjectObject),
Dir(String, Vec<ProjectObjectNode>),
}
fn find_dir<'a>(name: &str, nodes: &'a mut Vec<ProjectUnitNode>) -> &'a mut Vec<ProjectUnitNode> {
fn find_dir<'a>(
name: &str,
nodes: &'a mut Vec<ProjectObjectNode>,
) -> &'a mut Vec<ProjectObjectNode> {
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<ProjectUnitNode> {
fn build_nodes(objects: &[ProjectObject]) -> Vec<ProjectObjectNode> {
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<ProjectUnitNode> {
}
}
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(())
}

View File

@ -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<Box<CheckUpdateResult>> {
fn run_check_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
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<Box<CheckUp
pub fn start_check_update() -> 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)))
})
}

View File

@ -26,12 +26,22 @@ pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Default)]
pub struct JobQueue {
pub jobs: Vec<JobState>,
pub results: Vec<JobResult>,
}
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<RwLock<JobStatus>>;
pub struct JobState {
pub id: usize,
pub kind: Job,
pub handle: Option<JoinHandle<JobResult>>,
pub status: Arc<RwLock<JobStatus>>,
pub status: JobStatusRef,
pub cancel: Sender<()>,
pub should_remove: bool,
}
@ -99,8 +111,8 @@ pub struct JobStatus {
pub enum JobResult {
None,
ObjDiff(Box<ObjDiffResult>),
CheckUpdate(Box<CheckUpdateResult>),
ObjDiff(Option<Box<ObjDiffResult>>),
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
}
@ -111,12 +123,10 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
}
}
type Status = Arc<RwLock<JobStatus>>;
fn start_job(
title: &str,
kind: Job,
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
run: impl FnOnce(&JobStatusRef, Receiver<()>) -> Result<JobResult> + 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,

View File

@ -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<RwLock<AppConfig>>,
config: AppConfigRef,
) -> Result<Box<ObjDiffResult>> {
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<RwLock<AppConfig>>) -> 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)))
})
}

View File

@ -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<Box<UpdateResult>> {
fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
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()?;

View File

@ -7,7 +7,6 @@ pub struct Appearance {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
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,

View File

@ -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<Box<CheckUpdateResult>>,
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<String>,
pub unit_search: String,
pub object_search: String,
#[cfg(windows)]
pub available_wsl_distros: Option<Vec<String>>,
}
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<String> {
pub fn config_ui(
ui: &mut egui::Ui,
config: &Arc<RwLock<AppConfig>>,
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<String>,
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<String>,
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<ProjectUnitNode> {
fn filter_node(node: &ProjectObjectNode, search: &str) -> Option<ProjectObjectNode> {
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::<Vec<_>>();
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<RwLock<AppConfig>>,
config: &AppConfigRef,
show: &mut bool,
state: &mut ConfigViewState,
appearance: &Appearance,

View File

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

View File

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

View File

@ -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<Box<ObjDiffResult>>,
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<String>,
pub selected_symbol: Option<SymbolReference>,
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<String>,
selected_symbol: &mut Option<SymbolReference>,
current_view: &mut View,
state: &mut SymbolViewState,
appearance: &Appearance,
) {
) -> Option<View> {
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<String>,
selected_symbol: &mut Option<SymbolReference>,
current_view: &mut View,
state: &mut SymbolViewState,
lower_search: &str,
appearance: &Appearance,
) {
) -> Option<View> {
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 &section.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;
}
}