diff --git a/Cargo.lock b/Cargo.lock index 94a1591..697b024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2084,6 +2084,25 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -2904,6 +2923,7 @@ dependencies = [ "log", "notify", "objdiff-core", + "open", "path-slash", "png", "pollster", @@ -2941,6 +2961,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "open" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.66" @@ -3066,6 +3097,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pathfinder_geometry" version = "0.5.1" diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index de06707..c8826d4 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -124,6 +124,10 @@ impl ProjectObject { pub fn hidden(&self) -> bool { self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false) } + + pub fn source_path(&self) -> Option<&String> { + self.metadata.as_ref().and_then(|m| m.source_path.as_ref()) + } } #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] diff --git a/objdiff-gui/Cargo.toml b/objdiff-gui/Cargo.toml index 04e5697..9f1e073 100644 --- a/objdiff-gui/Cargo.toml +++ b/objdiff-gui/Cargo.toml @@ -40,6 +40,7 @@ globset = { version = "0.4", features = ["serde1"] } log = "0.4" notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" } objdiff-core = { path = "../objdiff-core", features = ["all"] } +open = "5.3" png = "0.17" pollster = "0.3" regex = "1.10" diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index 16928f4..0ca6e5e 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -73,6 +73,7 @@ pub struct ObjectConfig { pub reverse_fn_order: Option, pub complete: Option, pub scratch: Option, + pub source_path: Option, } #[inline] diff --git a/objdiff-gui/src/app_config.rs b/objdiff-gui/src/app_config.rs index 86ddf87..f686ab6 100644 --- a/objdiff-gui/src/app_config.rs +++ b/objdiff-gui/src/app_config.rs @@ -61,6 +61,7 @@ impl ObjectConfigV0 { reverse_fn_order: self.reverse_fn_order, complete: None, scratch: None, + source_path: None, } } } diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index cfdeaf6..a54b951 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -2,7 +2,7 @@ use std::string::FromUtf16Error; use std::{ mem::take, - path::{PathBuf, MAIN_SEPARATOR}, + path::{Path, PathBuf, MAIN_SEPARATOR}, }; #[cfg(all(windows, feature = "wsl"))] @@ -96,6 +96,7 @@ impl ConfigViewState { reverse_fn_order: None, complete: None, scratch: None, + source_path: None, }); } else if let Ok(obj_path) = path.strip_prefix(target_dir) { let base_path = base_dir.join(obj_path); @@ -106,6 +107,7 @@ impl ConfigViewState { reverse_fn_order: None, complete: None, scratch: None, + source_path: None, }); } } @@ -174,7 +176,10 @@ pub fn config_ui( ) { let mut state_guard = state.write().unwrap(); let AppState { - config: AppConfig { target_obj_dir, base_obj_dir, selected_obj, auto_update_check, .. }, + config: + AppConfig { + project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, .. + }, objects, object_nodes, .. @@ -318,7 +323,14 @@ pub fn config_ui( config_state.show_hidden, ) }) { - display_node(ui, &mut new_selected_obj, &node, appearance, node_open); + display_node( + ui, + &mut new_selected_obj, + project_dir.as_deref(), + &node, + appearance, + node_open, + ); } }); } @@ -333,13 +345,12 @@ pub fn config_ui( { config_state.queue_build = true; } - - ui.separator(); } fn display_object( ui: &mut egui::Ui, selected_obj: &mut Option, + project_dir: Option<&Path>, name: &str, object: &ProjectObject, appearance: &Appearance, @@ -357,7 +368,7 @@ fn display_object( } else { appearance.text_color }; - let clicked = SelectableLabel::new( + let response = SelectableLabel::new( selected, RichText::new(name) .font(FontId { @@ -366,11 +377,13 @@ fn display_object( }) .color(color), ) - .ui(ui) - .clicked(); + .ui(ui); + if get_source_path(project_dir, object).is_some() { + response.context_menu(|ui| object_context_ui(ui, object, project_dir)); + } // Always recreate ObjectConfig if selected, in case the project config changed. // ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. - if selected || clicked { + if selected || response.clicked() { *selected_obj = Some(ObjectConfig { name: object_name.to_string(), target_path: object.target_path.clone(), @@ -378,10 +391,31 @@ fn display_object( reverse_fn_order: object.reverse_fn_order(), complete: object.complete(), scratch: object.scratch.clone(), + source_path: object.source_path().cloned(), }); } } +fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option { + project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path))) +} + +fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) { + if let Some(source_path) = get_source_path(project_dir, object) { + if ui + .button("Open source file") + .on_hover_text("Open the source file in the default editor") + .clicked() + { + log::info!("Opening file {}", source_path.display()); + if let Err(e) = open::that_detached(&source_path) { + log::error!("Failed to open source file: {e}"); + } + ui.close_menu(); + } + } +} + #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] enum NodeOpen { #[default] @@ -394,13 +428,14 @@ enum NodeOpen { fn display_node( ui: &mut egui::Ui, selected_obj: &mut Option, + project_dir: Option<&Path>, node: &ProjectObjectNode, appearance: &Appearance, node_open: NodeOpen, ) { match node { ProjectObjectNode::File(name, object) => { - display_object(ui, selected_obj, name, object, appearance); + display_object(ui, selected_obj, project_dir, name, object, appearance); } ProjectObjectNode::Dir(name, children) => { let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); @@ -426,7 +461,7 @@ fn display_node( .open(open) .show(ui, |ui| { for node in children { - display_node(ui, selected_obj, node, appearance, node_open); + display_node(ui, selected_obj, project_dir, node, appearance, node_open); } }); } diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index b4a6172..8ff85fe 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -509,6 +509,18 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance ); } }); + ui.separator(); + if ui + .add_enabled( + state.source_path_available, + egui::Button::new("🖹 Source file"), + ) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + state.queue_open_source_path = true; + } }); ui.scope(|ui| { diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 65deef2..5107147 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -52,6 +52,8 @@ pub struct DiffViewState { pub scratch_available: bool, pub queue_scratch: bool, pub scratch_running: bool, + pub source_path_available: bool, + pub queue_open_source_path: bool, } #[derive(Default)] @@ -86,6 +88,9 @@ impl DiffViewState { self.symbol_state.reverse_fn_order = value; self.symbol_state.disable_reverse_fn_order = true; } + self.source_path_available = obj_config.source_path.is_some(); + } else { + self.source_path_available = false; } self.scratch_available = CreateScratchConfig::is_available(&state.config); } @@ -122,6 +127,22 @@ impl DiffViewState { } } } + + if self.queue_open_source_path { + self.queue_open_source_path = false; + if let Ok(state) = state.read() { + if let (Some(project_dir), Some(source_path)) = ( + &state.config.project_dir, + state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()), + ) { + let source_path = project_dir.join(source_path); + log::info!("Opening file {}", source_path.display()); + open::that_detached(source_path).unwrap_or_else(|err| { + log::error!("Failed to open source file: {err}"); + }); + } + } + } } } @@ -518,10 +539,27 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea |ui| { ui.set_width(column_width); + ui.horizontal(|ui| { + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + ui.label("Build base:"); + }); + ui.separator(); + if ui + .add_enabled( + state.source_path_available, + egui::Button::new("🖹 Source file"), + ) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + state.queue_open_source_path = true; + } + }); + ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - - ui.label("Build base:"); if result.second_status.success { if result.second_obj.is_none() { ui.colored_label(appearance.replace_color, "Missing");