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:
Luke Street 2025-09-23 12:11:40 -06:00 committed by GitHub
parent 1866158092
commit 56dac46280
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 414 additions and 105 deletions

10
Cargo.lock generated
View File

@ -1255,6 +1255,15 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "egui-wgpu" name = "egui-wgpu"
version = "0.32.3" version = "0.32.3"
@ -3564,6 +3573,7 @@ dependencies = [
"dirs", "dirs",
"eframe", "eframe",
"egui", "egui",
"egui-notify",
"egui_extras", "egui_extras",
"exec", "exec",
"filetime", "filetime",

View File

@ -111,6 +111,25 @@
"items": { "items": {
"$ref": "#/$defs/progress_category" "$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": { "$defs": {
@ -156,6 +175,20 @@
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
},
"options": {
"type": "object",
"description": "Diff configuration options that should be applied when this unit is active.",
"additionalProperties": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
}
} }
} }
}, },

View File

@ -24,7 +24,8 @@ use objdiff_core::{
watcher::{Watcher, create_watcher}, watcher::{Watcher, create_watcher},
}, },
config::{ 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}, path::{check_path_buf, platform_path, platform_path_serde_option},
}, },
diff::{DiffObjConfig, MappingConfig, ObjectDiff}, diff::{DiffObjConfig, MappingConfig, ObjectDiff},
@ -77,11 +78,11 @@ pub struct Args {
} }
pub fn run(args: Args) -> Result<()> { 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) { match (&args.target, &args.base, &args.project, &args.unit) {
(Some(_), Some(_), None, None) (Some(_), Some(_), None, None)
| (Some(_), None, 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) => { (None, None, p, u) => {
let project = match p { let project = match p {
Some(project) => project.clone(), Some(project) => project.clone(),
@ -106,28 +107,32 @@ pub fn run(args: Args) -> Result<()> {
.base_dir .base_dir
.as_ref() .as_ref()
.map(|p| project.join(p.with_platform_encoding())); .map(|p| project.join(p.with_platform_encoding()));
let objects = project_config let units = project_config.units.as_deref().unwrap_or_default();
.units let objects = units
.iter() .iter()
.flatten() .enumerate()
.map(|o| { .map(|(idx, o)| {
ObjectConfig::new( (
o, ObjectConfig::new(
&project, o,
target_obj_dir.as_deref(), &project,
base_obj_dir.as_deref(), target_obj_dir.as_deref(),
base_obj_dir.as_deref(),
),
idx,
) )
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let object = if let Some(u) = u { let (object, unit_idx) = if let Some(u) = u {
objects objects
.iter() .iter()
.find(|obj| obj.name == *u) .find(|(obj, _)| obj.name == *u)
.map(|(obj, idx)| (obj, *idx))
.ok_or_else(|| anyhow!("Unit not found: {}", u))? .ok_or_else(|| anyhow!("Unit not found: {}", u))?
} else if let Some(symbol_name) = &args.symbol { } else if let Some(symbol_name) = &args.symbol {
let mut idx = None; let mut idx = None;
let mut count = 0usize; let mut count = 0usize;
for (i, obj) in objects.iter().enumerate() { for (i, (obj, unit_idx)) in objects.iter().enumerate() {
if obj if obj
.target_path .target_path
.as_deref() .as_deref()
@ -135,7 +140,7 @@ pub fn run(args: Args) -> Result<()> {
.transpose()? .transpose()?
.unwrap_or(false) .unwrap_or(false)
{ {
idx = Some(i); idx = Some((i, *unit_idx));
count += 1; count += 1;
if count > 1 { if count > 1 {
break; break;
@ -144,7 +149,7 @@ pub fn run(args: Args) -> Result<()> {
} }
match (count, idx) { match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name), (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!( (2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit", "Multiple instances of {} were found, try specifying a unit",
symbol_name symbol_name
@ -154,18 +159,29 @@ pub fn run(args: Args) -> Result<()> {
} else { } else {
bail!("Must specify one of: symbol, project and unit, target and base objects") 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 target_path = object.target_path.clone();
let base_path = object.base_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"), _ => 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(); 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)?; apply_config_args(&mut diff_config, &args.config)?;
let mut mapping_config = MappingConfig { let mut mapping_config = MappingConfig {
mappings: Default::default(), mappings: Default::default(),
@ -316,11 +332,13 @@ fn run_interactive(
target_path: Option<Utf8PlatformPathBuf>, target_path: Option<Utf8PlatformPathBuf>,
base_path: Option<Utf8PlatformPathBuf>, base_path: Option<Utf8PlatformPathBuf>,
project_config: Option<ProjectConfig>, project_config: Option<ProjectConfig>,
unit_options: Option<ProjectOptions>,
) -> Result<()> { ) -> Result<()> {
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; 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]") let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
.context("Failed to parse time format")?; .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 { let mut state = AppState {
jobs: Default::default(), jobs: Default::default(),
waker: Default::default(), waker: Default::default(),

View File

@ -7,7 +7,7 @@ use objdiff_core::{
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION, ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION,
Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
}, },
config::path::platform_path, config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path},
diff, diff,
obj::{self, SectionKind, SymbolFlag, SymbolKind}, obj::{self, SectionKind, SymbolFlag, SymbolKind},
}; };
@ -83,14 +83,13 @@ pub fn run(args: Args) -> Result<()> {
} }
fn generate(args: GenerateArgs) -> Result<()> { fn generate(args: GenerateArgs) -> Result<()> {
let mut diff_config = diff::DiffObjConfig { let base_diff_config = diff::DiffObjConfig {
function_reloc_diffs: diff::FunctionRelocDiffs::None, function_reloc_diffs: diff::FunctionRelocDiffs::None,
combine_data_sections: true, combine_data_sections: true,
combine_text_sections: true, combine_text_sections: true,
ppc_calculate_pool_relocations: false, ppc_calculate_pool_relocations: false,
..Default::default() ..Default::default()
}; };
apply_config_args(&mut diff_config, &args.config)?;
let output_format = OutputFormat::from_option(args.format.as_deref())?; let output_format = OutputFormat::from_option(args.format.as_deref())?;
let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new(".")); 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), Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
None => bail!("No project configuration found"), 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 = let target_obj_dir =
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding())); project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let base_obj_dir = let base_obj_dir =
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding())); project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let objects = project let project_units = project.units.as_deref().unwrap_or_default();
.units let objects = project_units
.iter() .iter()
.flatten() .enumerate()
.map(|o| { .map(|(idx, o)| {
ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref()) (
ObjectConfig::new(
o,
project_dir,
target_obj_dir.as_deref(),
base_obj_dir.as_deref(),
),
idx,
)
}) })
.collect::<Vec<_>>(); .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 start = Instant::now();
let mut units = vec![]; let mut units = vec![];
let mut existing_functions: HashSet<String> = HashSet::new(); let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate { if args.deduplicate {
// If deduplicating, we need to run single-threaded // 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))? if let Some(unit) = report_object(object, &diff_config, Some(&mut existing_functions))?
{ {
units.push(unit); units.push(unit);
@ -134,7 +146,15 @@ fn generate(args: GenerateArgs) -> Result<()> {
} else { } else {
let vec = objects let vec = objects
.par_iter() .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>>>>()?; .collect::<Result<Vec<Option<ReportUnit>>>>()?;
units = vec.into_iter().flatten().collect(); units = vec.into_iter().flatten().collect();
} }
@ -156,6 +176,24 @@ fn generate(args: GenerateArgs) -> Result<()> {
Ok(()) 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( fn report_object(
object: &ObjectConfig, object: &ObjectConfig,
diff_config: &diff::DiffObjConfig, diff_config: &diff::DiffObjConfig,

View File

@ -1,6 +1,7 @@
pub mod path; pub mod path;
use alloc::{ use alloc::{
borrow::Cow,
collections::BTreeMap, collections::BTreeMap,
string::{String, ToString}, string::{String, ToString},
vec::Vec, vec::Vec,
@ -45,6 +46,8 @@ pub struct ProjectConfig {
pub units: Option<Vec<ProjectObject>>, pub units: Option<Vec<ProjectObject>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub progress_categories: Option<Vec<ProjectProgressCategory>>, pub progress_categories: Option<Vec<ProjectProgressCategory>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub options: Option<ProjectOptions>,
} }
impl ProjectConfig { impl ProjectConfig {
@ -116,6 +119,8 @@ pub struct ProjectObject {
pub metadata: Option<ProjectObjectMetadata>, pub metadata: Option<ProjectObjectMetadata>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub symbol_mappings: Option<BTreeMap<String, String>>, 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)] #[derive(Default, Clone)]
@ -143,6 +148,15 @@ pub struct ProjectProgressCategory {
pub name: String, 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 { impl ProjectObject {
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
if let Some(name) = &self.name { if let Some(name) = &self.name {
@ -179,6 +193,8 @@ impl ProjectObject {
pub fn auto_generated(&self) -> Option<bool> { pub fn auto_generated(&self) -> Option<bool> {
self.metadata.as_ref().and_then(|m| m.auto_generated) 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)] #[derive(Default, Clone, Eq, PartialEq)]
@ -310,3 +326,47 @@ pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
} }
builder.build() 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
}

View File

@ -32,6 +32,7 @@ cwdemangle = "1.0"
dirs = "6.0" dirs = "6.0"
egui = "0.32" egui = "0.32"
egui_extras = "0.32" egui_extras = "0.32"
egui-notify = "0.20"
filetime = "0.2" filetime = "0.2"
float-ord = "0.3" float-ord = "0.3"
font-kit = "0.14" font-kit = "0.14"

View File

@ -11,14 +11,15 @@ use std::{
time::Instant, time::Instant,
}; };
use egui::text::LayoutJob;
use filetime::FileTime; use filetime::FileTime;
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
build::watcher::{Watcher, create_watcher}, build::watcher::{Watcher, create_watcher},
config::{ config::{
ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, build_globset, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, apply_project_options,
default_ignore_patterns, default_watch_patterns, path::platform_path_serde_option, build_globset, default_ignore_patterns, default_watch_patterns,
save_project_config, path::platform_path_serde_option, save_project_config,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
jobs::{Job, JobQueue, JobResult}, jobs::{Job, JobQueue, JobResult},
@ -164,7 +165,7 @@ pub struct AppState {
pub selecting_left: Option<String>, pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for /// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>, pub selecting_right: Option<String>,
pub config_error: Option<String>, pub top_left_toasts: egui_notify::Toasts,
} }
impl Default for AppState { impl Default for AppState {
@ -183,11 +184,23 @@ impl Default for AppState {
last_mod_check: Instant::now(), last_mod_check: Instant::now(),
selecting_left: None, selecting_left: None,
selecting_right: 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)] #[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig { pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455 // TODO: https://github.com/ron-rs/ron/pull/455
@ -321,6 +334,24 @@ impl AppState {
self.selecting_right = None; 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) { pub fn set_selecting_left(&mut self, right: &str) {
let Some(object) = self.config.selected_obj.as_mut() else { let Some(object) = self.config.selected_obj.as_mut() else {
return; return;
@ -401,11 +432,22 @@ impl AppState {
match save_project_config(config, info) { match save_project_config(config, info) {
Ok(new_info) => *info = new_info, Ok(new_info) => *info = new_info,
Err(e) => { Err(e) => {
log::error!("Failed to save project config: {e}"); log::error!("Failed to save project config: {e:#}");
self.config_error = Some(format!("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>>; pub type AppStateRef = Arc<RwLock<AppState>>;
@ -548,12 +590,9 @@ impl App {
if state.config_change { if state.config_change {
state.config_change = false; state.config_change = false;
match load_project_config(state) { if let Err(e) = load_project_config(state) {
Ok(()) => state.config_error = None, log::error!("Failed to load project config: {e:#}");
Err(e) => { state.show_error_toast("Failed to load project config", &e);
log::error!("Failed to load project config: {e}");
state.config_error = Some(e.to_string());
}
} }
} }
@ -579,7 +618,10 @@ impl App {
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
}) { }) {
Ok(watcher) => self.watcher = Some(watcher), 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; state.watcher_change = false;
} }
@ -806,7 +848,7 @@ impl eframe::App for App {
let mut action = None; let mut action = None;
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
let state = state.read().unwrap(); 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); 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); graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, 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); self.post_update(ctx, action);
} }

View File

@ -1,6 +1,11 @@
use anyhow::Result; use anyhow::Result;
use globset::Glob; 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 typed_path::{Utf8UnixComponent, Utf8UnixPath};
use crate::app::{AppState, ObjectConfig}; 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.object_nodes = build_nodes(&mut state.objects);
state.current_project_config = Some(project_config); state.current_project_config = Some(project_config);
state.project_config_info = Some(info); 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 // Reload selected object
if let Some(selected_obj) = &state.config.selected_obj { if let Some(selected_obj) = &state.config.selected_obj {

View File

@ -106,7 +106,7 @@ pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig {
.as_ref() .as_ref()
.and_then(|obj| obj.base_path.as_ref()) .and_then(|obj| obj.base_path.as_ref())
.cloned(), .cloned(),
diff_obj_config: state.config.diff_obj_config.clone(), diff_obj_config: state.effective_diff_config(),
mapping_config: MappingConfig { mapping_config: MappingConfig {
mappings: state mappings: state
.config .config

View File

@ -519,8 +519,7 @@ fn format_path(path: &Option<Utf8PlatformPathBuf>, appearance: &Appearance) -> R
RichText::new(text).color(color).family(FontFamily::Monospace) RichText::new(text).color(color).family(FontFamily::Monospace)
} }
pub const CONFIG_DISABLED_TEXT: &str = pub const CONFIG_DISABLED_TEXT: &str = "Value is overridden by the project configuration.";
"Option disabled because it's set by the project configuration file.";
fn pick_folder_ui( fn pick_folder_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -533,8 +532,13 @@ fn pick_folder_ui(
let response = ui.horizontal(|ui| { let response = ui.horizontal(|ui| {
subheading(ui, label, appearance); subheading(ui, label, appearance);
ui.link(HELP_ICON).on_hover_ui(tooltip); ui.link(HELP_ICON).on_hover_ui(tooltip);
ui.add_enabled(enabled, egui::Button::new("Select")) let button = ui
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .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)); ui.label(format_path(dir, appearance));
response.inner response.inner
@ -552,17 +556,6 @@ pub fn project_window(
egui::Window::new("Project").open(show).show(ctx, |ui| { egui::Window::new("Project").open(show).show(ctx, |ui| {
split_obj_config_ui(ui, &mut state_guard, config_state, appearance); 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( fn split_obj_config_ui(
@ -623,6 +616,9 @@ fn split_obj_config_ui(
job.append(".", 0.0, text_format.clone()); job.append(".", 0.0, text_format.clone());
ui.label(job); 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(); let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
if ui if ui
@ -831,6 +827,9 @@ fn patterns_ui(
*patterns = on_reset(); *patterns = on_reset();
change = true; change = true;
} }
if has_project_config {
project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT);
}
}); });
let mut remove_at: Option<usize> = None; let mut remove_at: Option<usize> = None;
for (idx, glob) in patterns.iter().enumerate() { 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( fn config_property_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
state: &mut AppState, state: &mut AppState,
property_id: ConfigPropertyId, property_id: ConfigPropertyId,
) -> bool { ) -> bool {
let mut changed = false; let mut changed = false;
let current_value = state.config.diff_obj_config.get_property_value(property_id); let is_overridden = state.current_project_config.as_ref().is_some_and(|config| {
match (property_id.kind(), current_value) { let key = property_id.name();
(ConfigPropertyKind::Boolean, ConfigPropertyValue::Boolean(mut checked)) => { if let Some(selected) = state.config.selected_obj.as_ref()
let mut response = ui.checkbox(&mut checked, property_id.name()); && let Some(units) = config.units.as_deref()
if let Some(description) = property_id.description() { && let Some(unit) = units.iter().find(|unit| unit.name() == selected.name)
response = response.on_hover_text(description); && let Some(options) = unit.options()
} && options.contains_key(key)
if response.changed() { {
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 state
.config .config
.diff_obj_config .diff_obj_config
@ -907,7 +950,11 @@ fn config_property_ui(
changed = true; changed = true;
} }
} }
(ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => { (
ConfigPropertyKind::Choice(variants),
ConfigPropertyValue::Choice(base_selected),
override_value,
) => {
fn variant_name(variant: &ConfigEnumVariantInfo) -> String { fn variant_name(variant: &ConfigEnumVariantInfo) -> String {
if variant.is_default { if variant.is_default {
format!("{} (default)", variant.name) format!("{} (default)", variant.name)
@ -915,36 +962,51 @@ fn config_property_ui(
variant.name.to_string() variant.name.to_string()
} }
} }
let display_selected = match override_value {
Some(ConfigPropertyValue::Choice(value)) => value,
_ => base_selected,
};
let selected_variant = variants let selected_variant = variants
.iter() .iter()
.find(|v| v.value == selected) .find(|v| v.value == display_selected)
.or_else(|| variants.iter().find(|v| v.is_default)) .or_else(|| variants.iter().find(|v| v.is_default))
.expect("Invalid choice variant"); .expect("Invalid choice variant");
let response = egui::ComboBox::new(property_id.name(), property_id.name()) let mut new_value: Option<&'static str> = None;
.selected_text(variant_name(selected_variant)) ui.horizontal(|ui| {
.show_ui(ui, |ui| { let inner = ui.add_enabled_ui(!is_overridden, |ui| {
for variant in variants { egui::ComboBox::new(property_id.name(), property_id.name())
let mut response = .selected_text(variant_name(selected_variant))
ui.selectable_label(selected == variant.value, variant_name(variant)); .show_ui(ui, |ui| {
if let Some(description) = variant.description { for variant in variants {
response = response.on_hover_text(description); let mut response = ui.selectable_label(
} display_selected == variant.value,
if response.clicked() { variant_name(variant),
state );
.config if let Some(description) = variant.description {
.diff_obj_config response = response.on_hover_text(description);
.set_property_value( }
property_id, if response.clicked() {
ConfigPropertyValue::Choice(variant.value), new_value = Some(variant.value);
) }
.expect("Failed to set property value"); }
changed = true; });
} });
} let mut response = inner.response.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
}) if let Some(description) = property_id.description() {
.response; response = response.on_hover_text(description);
if let Some(description) = property_id.description() { }
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"), _ => panic!("Incompatible property kind and value"),

View File

@ -21,7 +21,11 @@ pub fn demangle_window(
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for demangler in Demangler::variants() { for demangler in Demangler::variants() {
if *demangler != Demangler::None { 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);
}
} }
} }
}); });