mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-09 13:37:55 +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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user