diff --git a/Cargo.lock b/Cargo.lock index a332571..7ac0b44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,9 +840,9 @@ dependencies = [ [[package]] name = "cwdemangle" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58d34a3a03cfe0a4ebfd03aeda6ee8a0f2e99bd3308476a8a89815add3ec373" +checksum = "c251bc5553377b3dc85c7b9b3955cfc2eb5a7b5544cf65adc2d53c2a4c2f4162" dependencies = [ "argh", ] @@ -2457,7 +2457,7 @@ dependencies = [ [[package]] name = "objdiff" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index 5acaa16..0acde2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "objdiff" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.65" authors = ["Luke Street "] @@ -27,7 +27,7 @@ byteorder = "1.4.3" bytes = "1.4.0" cfg-if = "1.0.0" const_format = "0.2.31" -cwdemangle = "0.1.5" +cwdemangle = "0.1.6" dirs = "5.0.1" eframe = { version = "0.22.0", features = ["persistence"] } egui = "0.22.0" diff --git a/README.md b/README.md index 47d39f6..ee8c2f4 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,11 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it // objdiff.json { "custom_make": "ninja", + + // Only required if objects use "path" instead of "target_path" and "base_path". "target_dir": "build/asm", "base_dir": "build/src", + "build_target": true, "watch_patterns": [ "*.c", @@ -63,8 +66,15 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it ], "objects": [ { + "name": "main/MetroTRK/mslsupp", + + // Option 1: Relative to target_dir and base_dir "path": "MetroTRK/mslsupp.o", - "name": "MetroTRK/mslsupp", + // Option 2: Explicit paths from project root + // Useful for more complex directory layouts + "target_path": "build/asm/MetroTRK/mslsupp.o", + "base_path": "build/src/MetroTRK/mslsupp.o", + "reverse_fn_order": false }, // ... @@ -72,25 +82,43 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it } ``` -- `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. +`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` _(optional)_: 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` _(optional)_: 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. +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. +If not specified, objdiff will use the default patterns listed above. + +`objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation. + +> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used. +> +> `path`: Relative path to the object from the `target_dir` and `base_dir`. +> Requires `target_dir` and `base_dir` to be specified. +> +> `target_path`: Path to the target object from the project root. +> Required if `path` is not specified. +> +> `base_path`: Path to the base object from the project root. +> Required if `path` is not specified. +> +> `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 diff --git a/src/app.rs b/src/app.rs index 35eeb34..ebf8075 100644 --- a/src/app.rs +++ b/src/app.rs @@ -40,6 +40,18 @@ pub struct ViewState { pub show_project_config: bool, } +/// The configuration for a single object file. +#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ObjectConfig { + pub name: String, + pub target_path: PathBuf, + pub base_path: PathBuf, + pub reverse_fn_order: Option, +} + +#[inline] +fn bool_true() -> bool { true } + #[derive(Default, Clone, serde::Deserialize, serde::Serialize)] pub struct AppConfig { pub custom_make: Option, @@ -47,9 +59,10 @@ pub struct AppConfig { pub project_dir: Option, pub target_obj_dir: Option, pub base_obj_dir: Option, - pub obj_path: Option, + pub selected_obj: Option, pub build_target: bool, - pub watcher_enabled: bool, + #[serde(default = "bool_true")] + pub rebuild_on_changes: bool, pub auto_update_check: bool, pub watch_patterns: Vec, @@ -65,6 +78,8 @@ pub struct AppConfig { pub obj_change: bool, #[serde(skip)] pub queue_build: bool, + #[serde(skip)] + pub project_config_loaded: bool, } impl AppConfig { @@ -72,7 +87,7 @@ impl AppConfig { self.project_dir = Some(path); self.target_obj_dir = None; self.base_obj_dir = None; - self.obj_path = None; + self.selected_obj = None; self.build_target = false; self.objects.clear(); self.object_nodes.clear(); @@ -80,24 +95,25 @@ impl AppConfig { self.config_change = true; self.obj_change = true; self.queue_build = false; + self.project_config_loaded = false; } pub fn set_target_obj_dir(&mut self, path: PathBuf) { self.target_obj_dir = Some(path); - self.obj_path = None; + self.selected_obj = 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.selected_obj = None; self.obj_change = true; self.queue_build = false; } - pub fn set_obj_path(&mut self, path: String) { - self.obj_path = Some(path); + pub fn set_selected_obj(&mut self, object: ObjectConfig) { + self.selected_obj = Some(object); self.obj_change = true; self.queue_build = false; } @@ -258,19 +274,19 @@ impl App { if config.obj_change { *diff_state = Default::default(); - if config.obj_path.is_some() { + if config.selected_obj.is_some() { config.queue_build = true; } config.obj_change = false; } - if self.modified.swap(false, Ordering::Relaxed) { + if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes { 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) { + if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) { jobs.push(start_build(self.config.clone())); config.queue_build = false; } diff --git a/src/config.rs b/src/config.rs index 226cf52..9ba719d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::{Context, Result}; use globset::{Glob, GlobSet, GlobSetBuilder}; -use crate::app::AppConfig; +use crate::{app::AppConfig, views::config::DEFAULT_WATCH_PATTERNS}; #[derive(Default, Clone, serde::Deserialize)] #[serde(default)] @@ -15,7 +15,7 @@ pub struct ProjectConfig { pub target_dir: Option, pub base_dir: Option, pub build_target: bool, - pub watch_patterns: Vec, + pub watch_patterns: Option>, #[serde(alias = "units")] pub objects: Vec, } @@ -23,10 +23,24 @@ pub struct ProjectConfig { #[derive(Default, Clone, serde::Deserialize)] pub struct ProjectObject { pub name: Option, - pub path: PathBuf, + pub path: Option, + pub target_path: Option, + pub base_path: Option, pub reverse_fn_order: Option, } +impl ProjectObject { + pub fn name(&self) -> &str { + if let Some(name) = &self.name { + name + } else if let Some(path) = &self.path { + path.to_str().unwrap_or("[invalid path]") + } else { + "[unknown]" + } + } +} + #[derive(Clone)] pub enum ProjectObjectNode { File(String, ProjectObject), @@ -53,11 +67,22 @@ fn find_dir<'a>( unreachable!(); } -fn build_nodes(objects: &[ProjectObject]) -> Vec { +fn build_nodes( + objects: &[ProjectObject], + project_dir: &PathBuf, + target_obj_dir: &Option, + base_obj_dir: &Option, +) -> Vec { let mut nodes = vec![]; for object in objects { let mut out_nodes = &mut nodes; - let path = object.name.as_ref().map(Path::new).unwrap_or(&object.path); + let path = if let Some(name) = &object.name { + Path::new(name) + } else if let Some(path) = &object.path { + path + } else { + continue; + }; if let Some(parent) = path.parent() { for component in parent.components() { if let Component::Normal(name) = component { @@ -66,8 +91,23 @@ fn build_nodes(objects: &[ProjectObject]) -> Vec { } } } + let mut object = object.clone(); + if let (Some(target_obj_dir), Some(path), None) = + (target_obj_dir, &object.path, &object.target_path) + { + object.target_path = Some(target_obj_dir.join(path)); + } else if let Some(path) = &object.target_path { + object.target_path = Some(project_dir.join(path)); + } + if let (Some(base_obj_dir), Some(path), None) = + (base_obj_dir, &object.path, &object.base_path) + { + object.base_path = Some(base_obj_dir.join(path)); + } else if let Some(path) = &object.base_path { + object.base_path = Some(project_dir.join(path)); + } let filename = path.file_name().unwrap().to_str().unwrap().to_string(); - out_nodes.push(ProjectObjectNode::File(filename, object.clone())); + out_nodes.push(ProjectObjectNode::File(filename, object)); } nodes } @@ -84,10 +124,14 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> { 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_target = project_config.build_target; - config.watch_patterns = project_config.watch_patterns; + config.watch_patterns = project_config.watch_patterns.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); + config.object_nodes = + build_nodes(&config.objects, project_dir, &config.target_obj_dir, &config.base_obj_dir); + config.project_config_loaded = true; } Ok(()) } diff --git a/src/jobs/objdiff.rs b/src/jobs/objdiff.rs index 2f12503..149ce99 100644 --- a/src/jobs/objdiff.rs +++ b/src/jobs/objdiff.rs @@ -1,6 +1,6 @@ use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver}; -use anyhow::{Context, Error, Result}; +use anyhow::{anyhow, Context, Error, Result}; use time::OffsetDateTime; use crate::{ @@ -76,47 +76,51 @@ fn run_build( 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"))?; + let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; let project_dir = config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?; - let mut target_path = config - .target_obj_dir - .as_ref() - .ok_or_else(|| Error::msg("Missing target obj dir"))? - .to_owned(); - target_path.push(obj_path); - let mut base_path = - config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned(); - base_path.push(obj_path); - let target_path_rel = target_path - .strip_prefix(project_dir) - .context("Failed to create relative target obj path")?; - let base_path_rel = - base_path.strip_prefix(project_dir).context("Failed to create relative base obj path")?; + let target_path_rel = obj_config.target_path.strip_prefix(project_dir).map_err(|_| { + anyhow!( + "Target path '{}' doesn't begin with '{}'", + obj_config.target_path.display(), + project_dir.display() + ) + })?; + let base_path_rel = obj_config.base_path.strip_prefix(project_dir).map_err(|_| { + anyhow!( + "Base path '{}' doesn't begin with '{}'", + obj_config.base_path.display(), + project_dir.display() + ) + })?; let total = if config.build_target { 5 } else { 4 }; let first_status = if config.build_target { - update_status(status, format!("Building target {obj_path}"), 0, total, &cancel)?; + update_status(status, format!("Building target {}", target_path_rel.display()), 0, total, &cancel)?; run_make(project_dir, target_path_rel, &config) } else { BuildStatus { success: true, log: String::new() } }; - update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?; + update_status(status, format!("Building base {}", base_path_rel.display()), 1, total, &cancel)?; let second_status = run_make(project_dir, base_path_rel, &config); let time = OffsetDateTime::now_utc(); let mut first_obj = if first_status.success { - update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?; - Some(elf::read(&target_path)?) + update_status(status, format!("Loading target {}", target_path_rel.display()), 2, total, &cancel)?; + Some(elf::read(&obj_config.target_path).with_context(|| { + format!("Failed to read object '{}'", obj_config.target_path.display()) + })?) } else { None }; let mut second_obj = if second_status.success { - update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?; - Some(elf::read(&base_path)?) + update_status(status, format!("Loading base {}", base_path_rel.display()), 3, total, &cancel)?; + Some(elf::read(&obj_config.base_path).with_context(|| { + format!("Failed to read object '{}'", obj_config.base_path.display()) + })?) } else { None }; diff --git a/src/views/config.rs b/src/views/config.rs index aa18883..261c693 100644 --- a/src/views/config.rs +++ b/src/views/config.rs @@ -17,7 +17,7 @@ use globset::Glob; use self_update::cargo_crate_version; use crate::{ - app::{AppConfig, AppConfigRef}, + app::{AppConfig, AppConfigRef, ObjectConfig}, config::{ProjectObject, ProjectObjectNode}, jobs::{ check_update::{start_check_update, CheckUpdateResult}, @@ -79,7 +79,7 @@ impl ConfigViewState { } } -const DEFAULT_WATCH_PATTERNS: &[&str] = &[ +pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ "*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", "*.inc", "*.py", "*.yml", "*.txt", "*.json", ]; @@ -129,7 +129,7 @@ pub fn config_ui( selected_wsl_distro, target_obj_dir, base_obj_dir, - obj_path, + selected_obj, auto_update_check, objects, object_nodes, @@ -205,9 +205,9 @@ 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 objects.is_empty() { + let mut new_selected_obj = selected_obj.clone(); + if objects.is_empty() { + if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if ui.button("Select object").clicked() { if let Some(path) = rfd::FileDialog::new() .set_directory(&target_dir) @@ -215,88 +215,99 @@ pub fn config_ui( .pick_file() { if let Ok(obj_path) = path.strip_prefix(&base_dir) { - new_build_obj = Some(obj_path.display().to_string()); + let target_path = target_dir.join(obj_path); + new_selected_obj = Some(ObjectConfig { + name: obj_path.display().to_string(), + target_path, + base_path: path, + reverse_fn_order: None, + }); } else if let Ok(obj_path) = path.strip_prefix(&target_dir) { - new_build_obj = Some(obj_path.display().to_string()); + let base_path = base_dir.join(obj_path); + new_selected_obj = Some(ObjectConfig { + name: obj_path.display().to_string(), + target_path: path, + base_path, + reverse_fn_order: None, + }); } } } - if let Some(obj) = obj_path { + if let Some(obj) = selected_obj { ui.label( - RichText::new(&*obj) + RichText::new(&obj.name) .color(appearance.replace_color) .family(FontFamily::Monospace), ); } } else { - let had_search = !state.object_search.is_empty(); - egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(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 mut root_open = None; - let mut node_open = NodeOpen::Default; - ui.horizontal(|ui| { - if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() { - root_open = Some(false); - node_open = NodeOpen::Close; - } - if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { - root_open = Some(true); - node_open = NodeOpen::Open; - } - if ui - .add_enabled(obj_path.is_some(), egui::Button::new("⌖").small()) - .on_hover_text_at_pointer("Current object") - .clicked() - { - root_open = Some(true); - node_open = NodeOpen::Object; - } - }); - if state.object_search.is_empty() { - if had_search { - root_open = Some(true); - node_open = NodeOpen::Object; - } - } else if !had_search { + let mut root_open = None; + let mut node_open = NodeOpen::Default; + ui.horizontal(|ui| { + if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() { + root_open = Some(false); + node_open = NodeOpen::Close; + } + if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { root_open = Some(true); node_open = NodeOpen::Open; } - - CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId { - size: appearance.ui_font.size, - family: appearance.code_font.family.clone(), - })) - .open(root_open) - .default_open(true) - .show(ui, |ui| { - let mut nodes = Cow::Borrowed(object_nodes); - if !state.object_search.is_empty() { - let search = state.object_search.to_ascii_lowercase(); - nodes = Cow::Owned( - object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(), - ); - } - - ui.style_mut().wrap = Some(false); - for node in nodes.iter() { - display_node(ui, &mut new_build_obj, node, appearance, node_open); - } - }); - } - - if new_build_obj != *obj_path { - if let Some(obj) = new_build_obj { - // Will set obj_changed, which will trigger a rebuild - config_guard.set_obj_path(obj); + if ui + .add_enabled(selected_obj.is_some(), egui::Button::new("⌖").small()) + .on_hover_text_at_pointer("Current object") + .clicked() + { + root_open = Some(true); + node_open = NodeOpen::Object; } + }); + if state.object_search.is_empty() { + if had_search { + root_open = Some(true); + node_open = NodeOpen::Object; + } + } else if !had_search { + root_open = Some(true); + node_open = NodeOpen::Open; } - if config_guard.obj_path.is_some() - && ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() - { - state.queue_build = true; + + CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId { + size: appearance.ui_font.size, + family: appearance.code_font.family.clone(), + })) + .open(root_open) + .default_open(true) + .show(ui, |ui| { + let mut nodes = Cow::Borrowed(object_nodes); + if !state.object_search.is_empty() { + let search = state.object_search.to_ascii_lowercase(); + nodes = Cow::Owned( + object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(), + ); + } + + ui.style_mut().wrap = Some(false); + for node in nodes.iter() { + display_node(ui, &mut new_selected_obj, node, appearance, node_open); + } + }); + } + if new_selected_obj != *selected_obj { + if let Some(obj) = new_selected_obj { + // Will set obj_changed, which will trigger a rebuild + config_guard.set_selected_obj(obj); } - } else { - ui.colored_label(appearance.delete_color, "Missing project settings"); + } + if config_guard.selected_obj.is_some() + && ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() + { + state.queue_build = true; } ui.separator(); @@ -304,13 +315,13 @@ pub fn config_ui( fn display_object( ui: &mut egui::Ui, - obj_path: &mut Option, + selected_obj: &mut Option, name: &str, object: &ProjectObject, appearance: &Appearance, ) { - let path_string = object.path.to_string_lossy().to_string(); - let selected = matches!(obj_path, Some(path) if path == &path_string); + let object_name = object.name(); + let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let color = if selected { appearance.emphasized_text_color } else { appearance.text_color }; if SelectableLabel::new( selected, @@ -324,7 +335,12 @@ fn display_object( .ui(ui) .clicked() { - *obj_path = Some(path_string); + *selected_obj = Some(ObjectConfig { + name: object_name.to_string(), + target_path: object.target_path.clone().unwrap_or_default(), + base_path: object.base_path.clone().unwrap_or_default(), + reverse_fn_order: object.reverse_fn_order, + }); } } @@ -339,17 +355,17 @@ enum NodeOpen { fn display_node( ui: &mut egui::Ui, - obj_path: &mut Option, + selected_obj: &mut Option, node: &ProjectObjectNode, appearance: &Appearance, node_open: NodeOpen, ) { match node { ProjectObjectNode::File(name, object) => { - display_object(ui, obj_path, name, object, appearance); + display_object(ui, selected_obj, name, object, appearance); } ProjectObjectNode::Dir(name, children) => { - let contains_obj = obj_path.as_ref().map(|path| contains_node(node, path)); + let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); let open = match node_open { NodeOpen::Default => None, NodeOpen::Open => Some(true), @@ -372,21 +388,18 @@ fn display_node( .open(open) .show(ui, |ui| { for node in children { - display_node(ui, obj_path, node, appearance, node_open); + display_node(ui, selected_obj, node, appearance, node_open); } }); } } } -fn contains_node(node: &ProjectObjectNode, path: &str) -> bool { +fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool { match node { - ProjectObjectNode::File(_, object) => { - let path_string = object.path.to_string_lossy().to_string(); - path == path_string - } + ProjectObjectNode::File(_, object) => object.name() == selected_obj.name, ProjectObjectNode::Dir(_, children) => { - children.iter().any(|node| contains_node(node, path)) + children.iter().any(|node| contains_node(node, selected_obj)) } } } @@ -444,11 +457,12 @@ fn pick_folder_ui( label: &str, tooltip: impl FnOnce(&mut egui::Ui), appearance: &Appearance, + enabled: bool, ) -> egui::Response { let response = ui.horizontal(|ui| { subheading(ui, label, appearance); ui.link(HELP_ICON).on_hover_ui(tooltip); - ui.button("Select") + ui.add_enabled(enabled, egui::Button::new("Select")) }); ui.label(format_path(dir, appearance)); response.inner @@ -506,6 +520,7 @@ fn split_obj_config_ui( ui.label(job); }, appearance, + true, ); if response.clicked() { if let Some(path) = rfd::FileDialog::new().pick_folder() { @@ -566,6 +581,7 @@ fn split_obj_config_ui( ui.label(job); }, appearance, + !config.project_config_loaded, ); if response.clicked() { if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { @@ -615,6 +631,7 @@ fn split_obj_config_ui( ui.label(job); }, appearance, + !config.project_config_loaded, ); if response.clicked() { if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { @@ -626,7 +643,7 @@ fn split_obj_config_ui( subheading(ui, "Watch settings", appearance); let response = - ui.checkbox(&mut config.watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| { + ui.checkbox(&mut 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.", diff --git a/src/views/jobs.rs b/src/views/jobs.rs index 13a5598..501c093 100644 --- a/src/views/jobs.rs +++ b/src/views/jobs.rs @@ -1,4 +1,4 @@ -use egui::{ProgressBar, Widget}; +use egui::{ProgressBar, RichText, Widget}; use crate::{jobs::JobQueue, views::appearance::Appearance}; @@ -31,7 +31,7 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) bar.ui(ui); const STATUS_LENGTH: usize = 80; if let Some(err) = &status.error { - let err_string = err.to_string(); + let err_string = format!("{:#}", err); ui.colored_label( appearance.delete_color, if err_string.len() > STATUS_LENGTH - 10 { @@ -39,13 +39,15 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) } 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); } }); } diff --git a/src/views/symbol_diff.rs b/src/views/symbol_diff.rs index 43dbd84..8a4a76d 100644 --- a/src/views/symbol_diff.rs +++ b/src/views/symbol_diff.rs @@ -62,15 +62,10 @@ impl DiffViewState { 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; - } + if let Some(obj_config) = &config.selected_obj { + if let Some(value) = obj_config.reverse_fn_order { + self.symbol_state.reverse_fn_order = value; + self.symbol_state.disable_reverse_fn_order = true; } } }