mirror of
				https://github.com/encounter/objdiff.git
				synced 2025-10-25 19:20:36 +00:00 
			
		
		
		
	Support overriding diff options in project config (& for individual units) (#263)
* Support loading diff options from project config * Support per-unit option overrides
This commit is contained in:
		
							parent
							
								
									1866158092
								
							
						
					
					
						commit
						56dac46280
					
				
							
								
								
									
										10
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1255,6 +1255,15 @@ dependencies = [ | ||||
|  "unicode-segmentation", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "egui-notify" | ||||
| version = "0.20.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3cd148c4c3fe05be0d9facf90add19a1531c1d7bfb9c7e4dbc179cfb31844d49" | ||||
| dependencies = [ | ||||
|  "egui", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "egui-wgpu" | ||||
| version = "0.32.3" | ||||
| @ -3564,6 +3573,7 @@ dependencies = [ | ||||
|  "dirs", | ||||
|  "eframe", | ||||
|  "egui", | ||||
|  "egui-notify", | ||||
|  "egui_extras", | ||||
|  "exec", | ||||
|  "filetime", | ||||
|  | ||||
| @ -111,6 +111,25 @@ | ||||
|       "items": { | ||||
|         "$ref": "#/$defs/progress_category" | ||||
|       } | ||||
|     }, | ||||
|     "options": { | ||||
|       "type": "object", | ||||
|       "description": "Diff configuration options that should be applied automatically when the project is loaded.", | ||||
|       "additionalProperties": { | ||||
|         "oneOf": [ | ||||
|           { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           { | ||||
|             "type": "string" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "examples": [ | ||||
|         { | ||||
|           "demangler": "gnu_legacy" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   "$defs": { | ||||
| @ -156,6 +175,20 @@ | ||||
|           "additionalProperties": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "options": { | ||||
|           "type": "object", | ||||
|           "description": "Diff configuration options that should be applied when this unit is active.", | ||||
|           "additionalProperties": { | ||||
|             "oneOf": [ | ||||
|               { | ||||
|                 "type": "boolean" | ||||
|               }, | ||||
|               { | ||||
|                 "type": "string" | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|  | ||||
| @ -24,7 +24,8 @@ use objdiff_core::{ | ||||
|         watcher::{Watcher, create_watcher}, | ||||
|     }, | ||||
|     config::{ | ||||
|         ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset, | ||||
|         ProjectConfig, ProjectObject, ProjectObjectMetadata, ProjectOptions, apply_project_options, | ||||
|         build_globset, | ||||
|         path::{check_path_buf, platform_path, platform_path_serde_option}, | ||||
|     }, | ||||
|     diff::{DiffObjConfig, MappingConfig, ObjectDiff}, | ||||
| @ -77,11 +78,11 @@ pub struct Args { | ||||
| } | ||||
| 
 | ||||
| pub fn run(args: Args) -> Result<()> { | ||||
|     let (target_path, base_path, project_config) = | ||||
|     let (target_path, base_path, project_config, unit_options) = | ||||
|         match (&args.target, &args.base, &args.project, &args.unit) { | ||||
|             (Some(_), Some(_), None, None) | ||||
|             | (Some(_), None, None, None) | ||||
|             | (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None), | ||||
|             | (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None, None), | ||||
|             (None, None, p, u) => { | ||||
|                 let project = match p { | ||||
|                     Some(project) => project.clone(), | ||||
| @ -106,28 +107,32 @@ pub fn run(args: Args) -> Result<()> { | ||||
|                     .base_dir | ||||
|                     .as_ref() | ||||
|                     .map(|p| project.join(p.with_platform_encoding())); | ||||
|                 let objects = project_config | ||||
|                     .units | ||||
|                 let units = project_config.units.as_deref().unwrap_or_default(); | ||||
|                 let objects = units | ||||
|                     .iter() | ||||
|                     .flatten() | ||||
|                     .map(|o| { | ||||
|                         ObjectConfig::new( | ||||
|                             o, | ||||
|                             &project, | ||||
|                             target_obj_dir.as_deref(), | ||||
|                             base_obj_dir.as_deref(), | ||||
|                     .enumerate() | ||||
|                     .map(|(idx, o)| { | ||||
|                         ( | ||||
|                             ObjectConfig::new( | ||||
|                                 o, | ||||
|                                 &project, | ||||
|                                 target_obj_dir.as_deref(), | ||||
|                                 base_obj_dir.as_deref(), | ||||
|                             ), | ||||
|                             idx, | ||||
|                         ) | ||||
|                     }) | ||||
|                     .collect::<Vec<_>>(); | ||||
|                 let object = if let Some(u) = u { | ||||
|                 let (object, unit_idx) = if let Some(u) = u { | ||||
|                     objects | ||||
|                         .iter() | ||||
|                         .find(|obj| obj.name == *u) | ||||
|                         .find(|(obj, _)| obj.name == *u) | ||||
|                         .map(|(obj, idx)| (obj, *idx)) | ||||
|                         .ok_or_else(|| anyhow!("Unit not found: {}", u))? | ||||
|                 } else if let Some(symbol_name) = &args.symbol { | ||||
|                     let mut idx = None; | ||||
|                     let mut count = 0usize; | ||||
|                     for (i, obj) in objects.iter().enumerate() { | ||||
|                     for (i, (obj, unit_idx)) in objects.iter().enumerate() { | ||||
|                         if obj | ||||
|                             .target_path | ||||
|                             .as_deref() | ||||
| @ -135,7 +140,7 @@ pub fn run(args: Args) -> Result<()> { | ||||
|                             .transpose()? | ||||
|                             .unwrap_or(false) | ||||
|                         { | ||||
|                             idx = Some(i); | ||||
|                             idx = Some((i, *unit_idx)); | ||||
|                             count += 1; | ||||
|                             if count > 1 { | ||||
|                                 break; | ||||
| @ -144,7 +149,7 @@ pub fn run(args: Args) -> Result<()> { | ||||
|                     } | ||||
|                     match (count, idx) { | ||||
|                         (0, None) => bail!("Symbol not found: {}", symbol_name), | ||||
|                         (1, Some(i)) => &objects[i], | ||||
|                         (1, Some((i, unit_idx))) => (&objects[i].0, unit_idx), | ||||
|                         (2.., Some(_)) => bail!( | ||||
|                             "Multiple instances of {} were found, try specifying a unit", | ||||
|                             symbol_name | ||||
| @ -154,18 +159,29 @@ pub fn run(args: Args) -> Result<()> { | ||||
|                 } else { | ||||
|                     bail!("Must specify one of: symbol, project and unit, target and base objects") | ||||
|                 }; | ||||
|                 let unit_options = units.get(unit_idx).and_then(|u| u.options().cloned()); | ||||
|                 let target_path = object.target_path.clone(); | ||||
|                 let base_path = object.base_path.clone(); | ||||
|                 (target_path, base_path, Some(project_config)) | ||||
|                 (target_path, base_path, Some(project_config), unit_options) | ||||
|             } | ||||
|             _ => bail!("Either target and base or project and unit must be specified"), | ||||
|         }; | ||||
| 
 | ||||
|     run_interactive(args, target_path, base_path, project_config) | ||||
|     run_interactive(args, target_path, base_path, project_config, unit_options) | ||||
| } | ||||
| 
 | ||||
| fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> { | ||||
| fn build_config_from_args( | ||||
|     args: &Args, | ||||
|     project_config: Option<&ProjectConfig>, | ||||
|     unit_options: Option<&ProjectOptions>, | ||||
| ) -> Result<(DiffObjConfig, MappingConfig)> { | ||||
|     let mut diff_config = DiffObjConfig::default(); | ||||
|     if let Some(options) = project_config.and_then(|config| config.options.as_ref()) { | ||||
|         apply_project_options(&mut diff_config, options)?; | ||||
|     } | ||||
|     if let Some(options) = unit_options { | ||||
|         apply_project_options(&mut diff_config, options)?; | ||||
|     } | ||||
|     apply_config_args(&mut diff_config, &args.config)?; | ||||
|     let mut mapping_config = MappingConfig { | ||||
|         mappings: Default::default(), | ||||
| @ -316,11 +332,13 @@ fn run_interactive( | ||||
|     target_path: Option<Utf8PlatformPathBuf>, | ||||
|     base_path: Option<Utf8PlatformPathBuf>, | ||||
|     project_config: Option<ProjectConfig>, | ||||
|     unit_options: Option<ProjectOptions>, | ||||
| ) -> Result<()> { | ||||
|     let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; | ||||
|     let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]") | ||||
|         .context("Failed to parse time format")?; | ||||
|     let (diff_obj_config, mapping_config) = build_config_from_args(&args)?; | ||||
|     let (diff_obj_config, mapping_config) = | ||||
|         build_config_from_args(&args, project_config.as_ref(), unit_options.as_ref())?; | ||||
|     let mut state = AppState { | ||||
|         jobs: Default::default(), | ||||
|         waker: Default::default(), | ||||
|  | ||||
| @ -7,7 +7,7 @@ use objdiff_core::{ | ||||
|         ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION, | ||||
|         Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, | ||||
|     }, | ||||
|     config::path::platform_path, | ||||
|     config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path}, | ||||
|     diff, | ||||
|     obj::{self, SectionKind, SymbolFlag, SymbolKind}, | ||||
| }; | ||||
| @ -83,14 +83,13 @@ pub fn run(args: Args) -> Result<()> { | ||||
| } | ||||
| 
 | ||||
| fn generate(args: GenerateArgs) -> Result<()> { | ||||
|     let mut diff_config = diff::DiffObjConfig { | ||||
|     let base_diff_config = diff::DiffObjConfig { | ||||
|         function_reloc_diffs: diff::FunctionRelocDiffs::None, | ||||
|         combine_data_sections: true, | ||||
|         combine_text_sections: true, | ||||
|         ppc_calculate_pool_relocations: false, | ||||
|         ..Default::default() | ||||
|     }; | ||||
|     apply_config_args(&mut diff_config, &args.config)?; | ||||
| 
 | ||||
|     let output_format = OutputFormat::from_option(args.format.as_deref())?; | ||||
|     let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new(".")); | ||||
| @ -101,31 +100,44 @@ fn generate(args: GenerateArgs) -> Result<()> { | ||||
|         Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err), | ||||
|         None => bail!("No project configuration found"), | ||||
|     }; | ||||
|     info!( | ||||
|         "Generating report for {} units (using {} threads)", | ||||
|         project.units().len(), | ||||
|         if args.deduplicate { 1 } else { rayon::current_num_threads() } | ||||
|     ); | ||||
| 
 | ||||
|     let target_obj_dir = | ||||
|         project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding())); | ||||
|     let base_obj_dir = | ||||
|         project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding())); | ||||
|     let objects = project | ||||
|         .units | ||||
|     let project_units = project.units.as_deref().unwrap_or_default(); | ||||
|     let objects = project_units | ||||
|         .iter() | ||||
|         .flatten() | ||||
|         .map(|o| { | ||||
|             ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref()) | ||||
|         .enumerate() | ||||
|         .map(|(idx, o)| { | ||||
|             ( | ||||
|                 ObjectConfig::new( | ||||
|                     o, | ||||
|                     project_dir, | ||||
|                     target_obj_dir.as_deref(), | ||||
|                     base_obj_dir.as_deref(), | ||||
|                 ), | ||||
|                 idx, | ||||
|             ) | ||||
|         }) | ||||
|         .collect::<Vec<_>>(); | ||||
|     info!( | ||||
|         "Generating report for {} units (using {} threads)", | ||||
|         objects.len(), | ||||
|         if args.deduplicate { 1 } else { rayon::current_num_threads() } | ||||
|     ); | ||||
| 
 | ||||
|     let start = Instant::now(); | ||||
|     let mut units = vec![]; | ||||
|     let mut existing_functions: HashSet<String> = HashSet::new(); | ||||
|     if args.deduplicate { | ||||
|         // If deduplicating, we need to run single-threaded
 | ||||
|         for object in &objects { | ||||
|         for (object, unit_idx) in &objects { | ||||
|             let diff_config = build_unit_diff_config( | ||||
|                 &base_diff_config, | ||||
|                 project.options.as_ref(), | ||||
|                 project_units.get(*unit_idx).and_then(ProjectObject::options), | ||||
|                 &args.config, | ||||
|             )?; | ||||
|             if let Some(unit) = report_object(object, &diff_config, Some(&mut existing_functions))? | ||||
|             { | ||||
|                 units.push(unit); | ||||
| @ -134,7 +146,15 @@ fn generate(args: GenerateArgs) -> Result<()> { | ||||
|     } else { | ||||
|         let vec = objects | ||||
|             .par_iter() | ||||
|             .map(|object| report_object(object, &diff_config, None)) | ||||
|             .map(|(object, unit_idx)| { | ||||
|                 let diff_config = build_unit_diff_config( | ||||
|                     &base_diff_config, | ||||
|                     project.options.as_ref(), | ||||
|                     project_units.get(*unit_idx).and_then(ProjectObject::options), | ||||
|                     &args.config, | ||||
|                 )?; | ||||
|                 report_object(object, &diff_config, None) | ||||
|             }) | ||||
|             .collect::<Result<Vec<Option<ReportUnit>>>>()?; | ||||
|         units = vec.into_iter().flatten().collect(); | ||||
|     } | ||||
| @ -156,6 +176,24 @@ fn generate(args: GenerateArgs) -> Result<()> { | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| fn build_unit_diff_config( | ||||
|     base: &diff::DiffObjConfig, | ||||
|     project_options: Option<&ProjectOptions>, | ||||
|     unit_options: Option<&ProjectOptions>, | ||||
|     cli_args: &[String], | ||||
| ) -> Result<diff::DiffObjConfig> { | ||||
|     let mut diff_config = base.clone(); | ||||
|     if let Some(options) = project_options { | ||||
|         apply_project_options(&mut diff_config, options)?; | ||||
|     } | ||||
|     if let Some(options) = unit_options { | ||||
|         apply_project_options(&mut diff_config, options)?; | ||||
|     } | ||||
|     // CLI args override project and unit options
 | ||||
|     apply_config_args(&mut diff_config, cli_args)?; | ||||
|     Ok(diff_config) | ||||
| } | ||||
| 
 | ||||
| fn report_object( | ||||
|     object: &ObjectConfig, | ||||
|     diff_config: &diff::DiffObjConfig, | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| pub mod path; | ||||
| 
 | ||||
| use alloc::{ | ||||
|     borrow::Cow, | ||||
|     collections::BTreeMap, | ||||
|     string::{String, ToString}, | ||||
|     vec::Vec, | ||||
| @ -45,6 +46,8 @@ pub struct ProjectConfig { | ||||
|     pub units: Option<Vec<ProjectObject>>, | ||||
|     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] | ||||
|     pub progress_categories: Option<Vec<ProjectProgressCategory>>, | ||||
|     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] | ||||
|     pub options: Option<ProjectOptions>, | ||||
| } | ||||
| 
 | ||||
| impl ProjectConfig { | ||||
| @ -116,6 +119,8 @@ pub struct ProjectObject { | ||||
|     pub metadata: Option<ProjectObjectMetadata>, | ||||
|     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] | ||||
|     pub symbol_mappings: Option<BTreeMap<String, String>>, | ||||
|     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] | ||||
|     pub options: Option<ProjectOptions>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Clone)] | ||||
| @ -143,6 +148,15 @@ pub struct ProjectProgressCategory { | ||||
|     pub name: String, | ||||
| } | ||||
| 
 | ||||
| pub type ProjectOptions = BTreeMap<String, ProjectOptionValue>; | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))] | ||||
| pub enum ProjectOptionValue { | ||||
|     Bool(bool), | ||||
|     String(String), | ||||
| } | ||||
| 
 | ||||
| impl ProjectObject { | ||||
|     pub fn name(&self) -> &str { | ||||
|         if let Some(name) = &self.name { | ||||
| @ -179,6 +193,8 @@ impl ProjectObject { | ||||
|     pub fn auto_generated(&self) -> Option<bool> { | ||||
|         self.metadata.as_ref().and_then(|m| m.auto_generated) | ||||
|     } | ||||
| 
 | ||||
|     pub fn options(&self) -> Option<&ProjectOptions> { self.options.as_ref() } | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Clone, Eq, PartialEq)] | ||||
| @ -310,3 +326,47 @@ pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> { | ||||
|     } | ||||
|     builder.build() | ||||
| } | ||||
| 
 | ||||
| #[cfg(feature = "any-arch")] | ||||
| pub fn apply_project_options( | ||||
|     diff_config: &mut crate::diff::DiffObjConfig, | ||||
|     options: &ProjectOptions, | ||||
| ) -> Result<()> { | ||||
|     use core::str::FromStr; | ||||
| 
 | ||||
|     use crate::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind}; | ||||
| 
 | ||||
|     let mut result = Ok(()); | ||||
|     for (key, value) in options.iter() { | ||||
|         let property_id = ConfigPropertyId::from_str(key) | ||||
|             .map_err(|()| anyhow!("Invalid configuration property: {key}"))?; | ||||
|         let value = match value { | ||||
|             ProjectOptionValue::Bool(value) => Cow::Borrowed(if *value { "true" } else { "false" }), | ||||
|             ProjectOptionValue::String(value) => Cow::Borrowed(value.as_str()), | ||||
|         }; | ||||
|         if diff_config.set_property_value_str(property_id, &value).is_err() { | ||||
|             if result.is_err() { | ||||
|                 // Already returning an error, skip further errors
 | ||||
|                 continue; | ||||
|             } | ||||
|             let mut expected = String::new(); | ||||
|             match property_id.kind() { | ||||
|                 ConfigPropertyKind::Boolean => expected.push_str("true, false"), | ||||
|                 ConfigPropertyKind::Choice(variants) => { | ||||
|                     for (idx, variant) in variants.iter().enumerate() { | ||||
|                         if idx > 0 { | ||||
|                             expected.push_str(", "); | ||||
|                         } | ||||
|                         expected.push_str(variant.value); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             result = Err(anyhow!( | ||||
|                 "Invalid value for {}. Expected one of: {}", | ||||
|                 property_id.name(), | ||||
|                 expected | ||||
|             )); | ||||
|         } | ||||
|     } | ||||
|     result | ||||
| } | ||||
|  | ||||
| @ -32,6 +32,7 @@ cwdemangle = "1.0" | ||||
| dirs = "6.0" | ||||
| egui = "0.32" | ||||
| egui_extras = "0.32" | ||||
| egui-notify = "0.20" | ||||
| filetime = "0.2" | ||||
| float-ord = "0.3" | ||||
| font-kit = "0.14" | ||||
|  | ||||
| @ -11,14 +11,15 @@ use std::{ | ||||
|     time::Instant, | ||||
| }; | ||||
| 
 | ||||
| use egui::text::LayoutJob; | ||||
| use filetime::FileTime; | ||||
| use globset::Glob; | ||||
| use objdiff_core::{ | ||||
|     build::watcher::{Watcher, create_watcher}, | ||||
|     config::{ | ||||
|         ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, build_globset, | ||||
|         default_ignore_patterns, default_watch_patterns, path::platform_path_serde_option, | ||||
|         save_project_config, | ||||
|         ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, apply_project_options, | ||||
|         build_globset, default_ignore_patterns, default_watch_patterns, | ||||
|         path::platform_path_serde_option, save_project_config, | ||||
|     }, | ||||
|     diff::DiffObjConfig, | ||||
|     jobs::{Job, JobQueue, JobResult}, | ||||
| @ -164,7 +165,7 @@ pub struct AppState { | ||||
|     pub selecting_left: Option<String>, | ||||
|     /// The left object symbol name that we're selecting a right symbol for
 | ||||
|     pub selecting_right: Option<String>, | ||||
|     pub config_error: Option<String>, | ||||
|     pub top_left_toasts: egui_notify::Toasts, | ||||
| } | ||||
| 
 | ||||
| impl Default for AppState { | ||||
| @ -183,11 +184,23 @@ impl Default for AppState { | ||||
|             last_mod_check: Instant::now(), | ||||
|             selecting_left: None, | ||||
|             selecting_right: None, | ||||
|             config_error: None, | ||||
|             top_left_toasts: create_toasts(egui_notify::Anchor::TopLeft), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn create_toasts(anchor: egui_notify::Anchor) -> egui_notify::Toasts { | ||||
|     egui_notify::Toasts::default() | ||||
|         .with_anchor(anchor) | ||||
|         .with_margin(egui::vec2(10.0, 32.0)) | ||||
|         .with_shadow(egui::Shadow { | ||||
|             offset: [0, 0], | ||||
|             blur: 0, | ||||
|             spread: 1, | ||||
|             color: egui::Color32::GRAY, | ||||
|         }) | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, serde::Deserialize, serde::Serialize)] | ||||
| pub struct AppConfig { | ||||
|     // TODO: https://github.com/ron-rs/ron/pull/455
 | ||||
| @ -321,6 +334,24 @@ impl AppState { | ||||
|         self.selecting_right = None; | ||||
|     } | ||||
| 
 | ||||
|     pub fn effective_diff_config(&self) -> DiffObjConfig { | ||||
|         let mut config = self.config.diff_obj_config.clone(); | ||||
|         if let Some(project) = self.current_project_config.as_ref() { | ||||
|             if let Some(options) = project.options.as_ref() { | ||||
|                 // Ignore errors here, we display them when loading the project config
 | ||||
|                 let _ = apply_project_options(&mut config, options); | ||||
|             } | ||||
|             if let Some(selected) = self.config.selected_obj.as_ref() | ||||
|                 && let Some(units) = project.units.as_deref() | ||||
|                 && let Some(unit) = units.iter().find(|unit| unit.name() == selected.name) | ||||
|                 && let Some(options) = unit.options() | ||||
|             { | ||||
|                 let _ = apply_project_options(&mut config, options); | ||||
|             } | ||||
|         } | ||||
|         config | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_selecting_left(&mut self, right: &str) { | ||||
|         let Some(object) = self.config.selected_obj.as_mut() else { | ||||
|             return; | ||||
| @ -401,11 +432,22 @@ impl AppState { | ||||
|         match save_project_config(config, info) { | ||||
|             Ok(new_info) => *info = new_info, | ||||
|             Err(e) => { | ||||
|                 log::error!("Failed to save project config: {e}"); | ||||
|                 self.config_error = Some(format!("Failed to save project config: {e}")); | ||||
|                 log::error!("Failed to save project config: {e:#}"); | ||||
|                 self.show_error_toast("Failed to save project config", &e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn show_error_toast(&mut self, context: &str, e: &anyhow::Error) { | ||||
|         let mut job = LayoutJob::default(); | ||||
|         job.append(context, 0.0, Default::default()); | ||||
|         job.append("\n", 0.0, Default::default()); | ||||
|         job.append(&format!("{e:#}"), 0.0, egui::TextFormat { | ||||
|             color: egui::Color32::LIGHT_RED, | ||||
|             ..Default::default() | ||||
|         }); | ||||
|         self.top_left_toasts.error(job).closable(true).duration(None); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub type AppStateRef = Arc<RwLock<AppState>>; | ||||
| @ -548,12 +590,9 @@ impl App { | ||||
| 
 | ||||
|         if state.config_change { | ||||
|             state.config_change = false; | ||||
|             match load_project_config(state) { | ||||
|                 Ok(()) => state.config_error = None, | ||||
|                 Err(e) => { | ||||
|                     log::error!("Failed to load project config: {e}"); | ||||
|                     state.config_error = Some(e.to_string()); | ||||
|                 } | ||||
|             if let Err(e) = load_project_config(state) { | ||||
|                 log::error!("Failed to load project config: {e:#}"); | ||||
|                 state.show_error_toast("Failed to load project config", &e); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -579,7 +618,10 @@ impl App { | ||||
|                         .map_err(anyhow::Error::new) | ||||
|                     }) { | ||||
|                     Ok(watcher) => self.watcher = Some(watcher), | ||||
|                     Err(e) => log::error!("Failed to create watcher: {e}"), | ||||
|                     Err(e) => { | ||||
|                         log::error!("Failed to create watcher: {e:#}"); | ||||
|                         state.show_error_toast("Failed to create file watcher", &e); | ||||
|                     } | ||||
|                 } | ||||
|                 state.watcher_change = false; | ||||
|             } | ||||
| @ -806,7 +848,7 @@ impl eframe::App for App { | ||||
|         let mut action = None; | ||||
|         egui::CentralPanel::default().show(ctx, |ui| { | ||||
|             let state = state.read().unwrap(); | ||||
|             action = diff_view_ui(ui, diff_state, appearance, &state.config.diff_obj_config); | ||||
|             action = diff_view_ui(ui, diff_state, appearance, &state.effective_diff_config()); | ||||
|         }); | ||||
| 
 | ||||
|         project_window(ctx, state, show_project_config, config_state, appearance); | ||||
| @ -818,6 +860,10 @@ impl eframe::App for App { | ||||
|         graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance); | ||||
|         jobs_window(ctx, show_jobs, jobs, appearance); | ||||
| 
 | ||||
|         if let Ok(mut state) = self.state.write() { | ||||
|             state.top_left_toasts.show(ctx); | ||||
|         } | ||||
| 
 | ||||
|         self.post_update(ctx, action); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,11 @@ | ||||
| use anyhow::Result; | ||||
| use globset::Glob; | ||||
| use objdiff_core::config::{default_ignore_patterns, default_watch_patterns, try_project_config}; | ||||
| use objdiff_core::{ | ||||
|     config::{ | ||||
|         apply_project_options, default_ignore_patterns, default_watch_patterns, try_project_config, | ||||
|     }, | ||||
|     diff::DiffObjConfig, | ||||
| }; | ||||
| use typed_path::{Utf8UnixComponent, Utf8UnixPath}; | ||||
| 
 | ||||
| use crate::app::{AppState, ObjectConfig}; | ||||
| @ -124,6 +129,38 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> { | ||||
|         state.object_nodes = build_nodes(&mut state.objects); | ||||
|         state.current_project_config = Some(project_config); | ||||
|         state.project_config_info = Some(info); | ||||
|         if let Some(options) = | ||||
|             state.current_project_config.as_ref().and_then(|project| project.options.as_ref()) | ||||
|         { | ||||
|             let mut diff_config = DiffObjConfig::default(); | ||||
|             if let Err(e) = apply_project_options(&mut diff_config, options) { | ||||
|                 log::error!("Failed to apply project config options: {e:#}"); | ||||
|                 state.show_error_toast("Failed to apply project config options", &e); | ||||
|             } | ||||
|         } | ||||
|         if let Some(project) = state.current_project_config.as_ref() | ||||
|             && let Some(units) = project.units.as_deref() | ||||
|         { | ||||
|             let mut unit_option_errors = Vec::new(); | ||||
|             for unit in units { | ||||
|                 if let Some(options) = unit.options() { | ||||
|                     let mut diff_config = DiffObjConfig::default(); | ||||
|                     if let Err(e) = apply_project_options(&mut diff_config, options) { | ||||
|                         unit_option_errors.push((unit.name().to_string(), e)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             for (unit_name, error) in unit_option_errors { | ||||
|                 log::error!( | ||||
|                     "Failed to apply project config options for unit {}: {error:#}", | ||||
|                     unit_name | ||||
|                 ); | ||||
|                 state.show_error_toast( | ||||
|                     &format!("Failed to apply project config options for unit {unit_name}"), | ||||
|                     &error, | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Reload selected object
 | ||||
|         if let Some(selected_obj) = &state.config.selected_obj { | ||||
|  | ||||
| @ -106,7 +106,7 @@ pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig { | ||||
|             .as_ref() | ||||
|             .and_then(|obj| obj.base_path.as_ref()) | ||||
|             .cloned(), | ||||
|         diff_obj_config: state.config.diff_obj_config.clone(), | ||||
|         diff_obj_config: state.effective_diff_config(), | ||||
|         mapping_config: MappingConfig { | ||||
|             mappings: state | ||||
|                 .config | ||||
|  | ||||
| @ -519,8 +519,7 @@ fn format_path(path: &Option<Utf8PlatformPathBuf>, appearance: &Appearance) -> R | ||||
|     RichText::new(text).color(color).family(FontFamily::Monospace) | ||||
| } | ||||
| 
 | ||||
| pub const CONFIG_DISABLED_TEXT: &str = | ||||
|     "Option disabled because it's set by the project configuration file."; | ||||
| pub const CONFIG_DISABLED_TEXT: &str = "Value is overridden by the project configuration."; | ||||
| 
 | ||||
| fn pick_folder_ui( | ||||
|     ui: &mut egui::Ui, | ||||
| @ -533,8 +532,13 @@ fn pick_folder_ui( | ||||
|     let response = ui.horizontal(|ui| { | ||||
|         subheading(ui, label, appearance); | ||||
|         ui.link(HELP_ICON).on_hover_ui(tooltip); | ||||
|         ui.add_enabled(enabled, egui::Button::new("Select")) | ||||
|             .on_disabled_hover_text(CONFIG_DISABLED_TEXT) | ||||
|         let button = ui | ||||
|             .add_enabled(enabled, egui::Button::new("Select")) | ||||
|             .on_disabled_hover_text(CONFIG_DISABLED_TEXT); | ||||
|         if !enabled { | ||||
|             project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); | ||||
|         } | ||||
|         button | ||||
|     }); | ||||
|     ui.label(format_path(dir, appearance)); | ||||
|     response.inner | ||||
| @ -552,17 +556,6 @@ pub fn project_window( | ||||
|     egui::Window::new("Project").open(show).show(ctx, |ui| { | ||||
|         split_obj_config_ui(ui, &mut state_guard, config_state, appearance); | ||||
|     }); | ||||
| 
 | ||||
|     if let Some(error) = &state_guard.config_error { | ||||
|         let mut open = true; | ||||
|         egui::Window::new("Error").open(&mut open).show(ctx, |ui| { | ||||
|             ui.label("Failed to load project config:"); | ||||
|             ui.colored_label(appearance.delete_color, error); | ||||
|         }); | ||||
|         if !open { | ||||
|             state_guard.config_error = None; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn split_obj_config_ui( | ||||
| @ -623,6 +616,9 @@ fn split_obj_config_ui( | ||||
|             job.append(".", 0.0, text_format.clone()); | ||||
|             ui.label(job); | ||||
|         }); | ||||
|         if state.project_config_info.is_some() { | ||||
|             project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); | ||||
|         } | ||||
|     }); | ||||
|     let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default(); | ||||
|     if ui | ||||
| @ -831,6 +827,9 @@ fn patterns_ui( | ||||
|             *patterns = on_reset(); | ||||
|             change = true; | ||||
|         } | ||||
|         if has_project_config { | ||||
|             project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); | ||||
|         } | ||||
|     }); | ||||
|     let mut remove_at: Option<usize> = None; | ||||
|     for (idx, glob) in patterns.iter().enumerate() { | ||||
| @ -885,20 +884,64 @@ pub fn arch_config_window( | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| fn project_override_badge(ui: &mut egui::Ui) -> egui::Response { | ||||
|     ui.add(egui::Label::new(RichText::new("⛭").color(ui.visuals().warn_fg_color)).selectable(false)) | ||||
| } | ||||
| 
 | ||||
| fn config_property_ui( | ||||
|     ui: &mut egui::Ui, | ||||
|     state: &mut AppState, | ||||
|     property_id: ConfigPropertyId, | ||||
| ) -> bool { | ||||
|     let mut changed = false; | ||||
|     let current_value = state.config.diff_obj_config.get_property_value(property_id); | ||||
|     match (property_id.kind(), current_value) { | ||||
|         (ConfigPropertyKind::Boolean, ConfigPropertyValue::Boolean(mut checked)) => { | ||||
|             let mut response = ui.checkbox(&mut checked, property_id.name()); | ||||
|             if let Some(description) = property_id.description() { | ||||
|                 response = response.on_hover_text(description); | ||||
|             } | ||||
|             if response.changed() { | ||||
|     let is_overridden = state.current_project_config.as_ref().is_some_and(|config| { | ||||
|         let key = property_id.name(); | ||||
|         if let Some(selected) = state.config.selected_obj.as_ref() | ||||
|             && let Some(units) = config.units.as_deref() | ||||
|             && let Some(unit) = units.iter().find(|unit| unit.name() == selected.name) | ||||
|             && let Some(options) = unit.options() | ||||
|             && options.contains_key(key) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|         if let Some(options) = config.options.as_ref() | ||||
|             && options.contains_key(key) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|         false | ||||
|     }); | ||||
|     let override_value = | ||||
|         is_overridden.then(|| state.effective_diff_config().get_property_value(property_id)); | ||||
|     let base_value = state.config.diff_obj_config.get_property_value(property_id); | ||||
|     match (property_id.kind(), base_value, override_value) { | ||||
|         ( | ||||
|             ConfigPropertyKind::Boolean, | ||||
|             ConfigPropertyValue::Boolean(base_checked), | ||||
|             override_value, | ||||
|         ) => { | ||||
|             let mut checked = match override_value { | ||||
|                 Some(ConfigPropertyValue::Boolean(value)) => value, | ||||
|                 _ => base_checked, | ||||
|             }; | ||||
|             let response = ui | ||||
|                 .horizontal(|ui| { | ||||
|                     let mut response = ui | ||||
|                         .add_enabled( | ||||
|                             !is_overridden, | ||||
|                             egui::widgets::Checkbox::new(&mut checked, property_id.name()), | ||||
|                         ) | ||||
|                         .on_disabled_hover_text(CONFIG_DISABLED_TEXT); | ||||
|                     if let Some(description) = property_id.description() { | ||||
|                         response = response.on_hover_text(description); | ||||
|                     } | ||||
|                     if is_overridden { | ||||
|                         project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); | ||||
|                     } | ||||
|                     response | ||||
|                 }) | ||||
|                 .inner; | ||||
|             if !is_overridden && response.changed() { | ||||
|                 state | ||||
|                     .config | ||||
|                     .diff_obj_config | ||||
| @ -907,7 +950,11 @@ fn config_property_ui( | ||||
|                 changed = true; | ||||
|             } | ||||
|         } | ||||
|         (ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => { | ||||
|         ( | ||||
|             ConfigPropertyKind::Choice(variants), | ||||
|             ConfigPropertyValue::Choice(base_selected), | ||||
|             override_value, | ||||
|         ) => { | ||||
|             fn variant_name(variant: &ConfigEnumVariantInfo) -> String { | ||||
|                 if variant.is_default { | ||||
|                     format!("{} (default)", variant.name) | ||||
| @ -915,36 +962,51 @@ fn config_property_ui( | ||||
|                     variant.name.to_string() | ||||
|                 } | ||||
|             } | ||||
|             let display_selected = match override_value { | ||||
|                 Some(ConfigPropertyValue::Choice(value)) => value, | ||||
|                 _ => base_selected, | ||||
|             }; | ||||
|             let selected_variant = variants | ||||
|                 .iter() | ||||
|                 .find(|v| v.value == selected) | ||||
|                 .find(|v| v.value == display_selected) | ||||
|                 .or_else(|| variants.iter().find(|v| v.is_default)) | ||||
|                 .expect("Invalid choice variant"); | ||||
|             let response = egui::ComboBox::new(property_id.name(), property_id.name()) | ||||
|                 .selected_text(variant_name(selected_variant)) | ||||
|                 .show_ui(ui, |ui| { | ||||
|                     for variant in variants { | ||||
|                         let mut response = | ||||
|                             ui.selectable_label(selected == variant.value, variant_name(variant)); | ||||
|                         if let Some(description) = variant.description { | ||||
|                             response = response.on_hover_text(description); | ||||
|                         } | ||||
|                         if response.clicked() { | ||||
|                             state | ||||
|                                 .config | ||||
|                                 .diff_obj_config | ||||
|                                 .set_property_value( | ||||
|                                     property_id, | ||||
|                                     ConfigPropertyValue::Choice(variant.value), | ||||
|                                 ) | ||||
|                                 .expect("Failed to set property value"); | ||||
|                             changed = true; | ||||
|                         } | ||||
|                     } | ||||
|                 }) | ||||
|                 .response; | ||||
|             if let Some(description) = property_id.description() { | ||||
|                 response.on_hover_text(description); | ||||
|             let mut new_value: Option<&'static str> = None; | ||||
|             ui.horizontal(|ui| { | ||||
|                 let inner = ui.add_enabled_ui(!is_overridden, |ui| { | ||||
|                     egui::ComboBox::new(property_id.name(), property_id.name()) | ||||
|                         .selected_text(variant_name(selected_variant)) | ||||
|                         .show_ui(ui, |ui| { | ||||
|                             for variant in variants { | ||||
|                                 let mut response = ui.selectable_label( | ||||
|                                     display_selected == variant.value, | ||||
|                                     variant_name(variant), | ||||
|                                 ); | ||||
|                                 if let Some(description) = variant.description { | ||||
|                                     response = response.on_hover_text(description); | ||||
|                                 } | ||||
|                                 if response.clicked() { | ||||
|                                     new_value = Some(variant.value); | ||||
|                                 } | ||||
|                             } | ||||
|                         }); | ||||
|                 }); | ||||
|                 let mut response = inner.response.on_disabled_hover_text(CONFIG_DISABLED_TEXT); | ||||
|                 if let Some(description) = property_id.description() { | ||||
|                     response = response.on_hover_text(description); | ||||
|                 } | ||||
|                 if is_overridden { | ||||
|                     project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); | ||||
|                 } | ||||
|                 response | ||||
|             }); | ||||
|             if !is_overridden && let Some(value) = new_value { | ||||
|                 state | ||||
|                     .config | ||||
|                     .diff_obj_config | ||||
|                     .set_property_value(property_id, ConfigPropertyValue::Choice(value)) | ||||
|                     .expect("Failed to set property value"); | ||||
|                 changed = true; | ||||
|             } | ||||
|         } | ||||
|         _ => panic!("Incompatible property kind and value"), | ||||
|  | ||||
| @ -21,7 +21,11 @@ pub fn demangle_window( | ||||
|             .show_ui(ui, |ui| { | ||||
|                 for demangler in Demangler::variants() { | ||||
|                     if *demangler != Demangler::None { | ||||
|                         ui.selectable_value(&mut state.demangler, *demangler, demangler.name()); | ||||
|                         let response = | ||||
|                             ui.selectable_value(&mut state.demangler, *demangler, demangler.name()); | ||||
|                         if let Some(description) = demangler.description() { | ||||
|                             response.on_hover_text(description); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user