Compare commits

..

18 Commits

Author SHA1 Message Date
Luke Street 0899e6779c Add alt shortcut support to many elements 2024-12-01 22:36:15 -07:00
LagoLunatic 95528fa8d2 Found a better place to clear the autoscroll flag
DiffViewState::post_update is where the flag gets set, so clearing it right before that at the start of the function seems to make the most sense, instead of doing it in App::update.
2024-12-01 22:36:15 -07:00
LagoLunatic 517b84e766 Simplify clearing of the autoscroll flag, remove &mut State 2024-12-01 22:36:15 -07:00
LagoLunatic 45dd73f5a9 Fix auto-scrolling to highlighted symbol only working for the left side
The flag is cleared after one scroll to avoid doing it continuously, but this breaks when we need to scroll to both the left and the right symbol at the same time. So now each side has its own flag to keep track of this state independently.
2024-12-01 22:36:15 -07:00
LagoLunatic d7d7a7f14a Add escape as an alternative to back hotkey 2024-12-01 22:36:15 -07:00
LagoLunatic 441b30070e Split function diff view: Enable PageUp/PageDown/Home/End for scrolling 2024-12-01 22:36:15 -07:00
LagoLunatic 28bd7182d1 Add hotkeys to change target and base functions 2024-12-01 22:36:13 -07:00
LagoLunatic 3f03a75825 Add space as alternative to enter hotkey
This is for consistency with egui's builtint enter/space hotkey for interacting with the focused widget.
2024-12-01 22:36:05 -07:00
LagoLunatic 4fb64a3ad4 Add Ctrl+F/S shortcuts for focusing the object and symbol filter text edits 2024-12-01 22:36:05 -07:00
LagoLunatic 77c104c67b Fix some hotkeys stealing input from focused widgets
e.g. The symbol list was stealing the W/S key presses when typing into the symbol filter text edit.

If the user actually wants to use these shortcuts while a widget is focused, they can simply press the escape key to unfocus all widgets and then press the shortcut.
2024-12-01 22:36:05 -07:00
LagoLunatic 046f3d6999 Auto-scroll the keyboard-selected symbols into view if offscreen 2024-12-01 22:36:05 -07:00
LagoLunatic 2427b06584 Do not clear highlighted symbol when hovering mouse over an unpaired symbol 2024-12-01 22:36:05 -07:00
LagoLunatic 157e99de6f Do not clear highlighted symbol when backing out of diff view 2024-12-01 22:36:05 -07:00
LagoLunatic b571787732 Add hotkeys to select the next symbol above/below the current one in the listing 2024-12-01 22:36:05 -07:00
LagoLunatic dbf86ec3cf Add scroll hotkeys 2024-12-01 22:36:05 -07:00
LagoLunatic acc1189150 Add enter and back hotkeys 2024-12-01 22:36:05 -07:00
LagoLunatic 123253c322 Update .gitignore 2024-12-01 22:36:05 -07:00
LagoLunatic ef680a5e7d Fix missing dependency feature for objdiff-gui 2024-12-01 22:36:05 -07:00
32 changed files with 1599 additions and 1975 deletions
Generated
+80 -101
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.5.0"
version = "2.4.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
+1 -1
View File
@@ -14,7 +14,7 @@ publish = false
[dependencies]
anyhow = "1.0"
argp = "0.4"
argp = "0.3"
crossterm = "0.28"
enable-ansi-support = "0.2"
memmap2 = "0.9"
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1,7 +1,6 @@
mod argp_version;
mod cmd;
mod util;
mod views;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
use anyhow::Result;
use crossterm::event::Event;
use ratatui::Frame;
use crate::cmd::diff::AppState;
pub mod function_diff;
#[derive(Default)]
pub struct EventResult {
pub redraw: bool,
pub click_xy: Option<(u16, u16)>,
}
pub enum EventControlFlow {
Break,
Continue(EventResult),
Reload,
}
pub trait UiView {
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult);
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow;
fn reload(&mut self, state: &AppState) -> Result<()>;
}
+2 -24
View File
@@ -16,10 +16,8 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"]
[features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings", "build"]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings"]
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
build = ["dep:shell-escape", "dep:path-slash", "dep:winapi", "dep:notify", "dep:notify-debouncer-full", "dep:reqwest", "dep:self_update", "dep:tempfile", "dep:time"]
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["dep:gimli"]
mips = ["any-arch", "dep:rabbitizer"]
@@ -27,6 +25,7 @@ ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
[package.metadata.docs.rs]
@@ -82,27 +81,6 @@ arm-attr = { version = "0.1", optional = true }
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
# build
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true }
notify-debouncer-full = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true }
shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.14", optional = true }
time = { version = "0.3", optional = true }
[target.'cfg(windows)'.dependencies]
path-slash = { version = "0.2", optional = true }
winapi = { version = "0.3", optional = true }
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true }
self_update = { version = "0.41", default-features = false, features = ["rustls"], optional = true }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
self_update = { version = "0.41", optional = true }
[build-dependencies]
prost-build = { version = "0.13", optional = true }
pbjson-build = { version = "0.7", optional = true }
+7 -16
View File
@@ -36,22 +36,13 @@ pub enum DataType {
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!("Failed to display a symbol value for a symbol whose size is too small for instruction referencing it.");
// TODO: Attempt to interpret large symbols as arrays of a smaller type,
// fallback to intrepreting it as bytes.
// https://github.com/encounter/objdiff/issues/124
if self.required_len().is_some_and(|l| bytes.len() != l) {
log::warn!("Failed to display a symbol value for a symbol whose size doesn't match the instruction referencing it.");
return None;
}
let mut bytes = bytes;
if self.required_len().is_some_and(|l| bytes.len() > l) {
// If the symbol's size is larger a single instance of this data type, we take just the
// bytes necessary for one of them in order to display the first element of the array.
bytes = &bytes[0..self.required_len().unwrap()];
// TODO: Attempt to interpret large symbols as arrays of a smaller type and show all
// elements of the array instead. https://github.com/encounter/objdiff/issues/124
// However, note that the stride of an array can not always be determined just by the
// data type guessed by the single instruction accessing it. There can also be arrays of
// structs that contain multiple elements of different types, so if other elements after
// the first one were to be displayed in this manner, they may be inaccurate.
}
match self {
DataType::Int8 => {
@@ -95,10 +86,10 @@ impl DataType {
}
}
DataType::Float => {
format!("Float: {:?}f", Endian::read_f32(bytes))
format!("Float: {}", Endian::read_f32(bytes))
}
DataType::Double => {
format!("Double: {:?}", Endian::read_f64(bytes))
format!("Double: {}", Endian::read_f64(bytes))
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
+19 -214
View File
@@ -1,7 +1,4 @@
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
};
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, ensure, Result};
use byteorder::BigEndian;
@@ -10,7 +7,7 @@ use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind,
};
use ppc750cl::{Argument, InsIter, Opcode, ParsedIns, GPR};
use ppc750cl::{Argument, InsIter, Opcode, GPR};
use crate::{
arch::{DataType, ObjArch, ProcessCodeResult},
@@ -52,8 +49,6 @@ impl ObjArch for ObjArchPpc {
let ins_count = code.len() / 4;
let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let fake_pool_reloc_for_addr =
generate_fake_pool_reloc_for_addr_mapping(address, code, relocations);
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc {
@@ -150,7 +145,7 @@ impl ObjArch for ObjArchPpc {
size: 4,
mnemonic: Cow::Borrowed(simplified.mnemonic),
args,
reloc: reloc.or(fake_pool_reloc_for_addr.get(&cur_addr)).cloned(),
reloc: reloc.cloned(),
op: ins.op as u16,
branch_dest,
line,
@@ -178,7 +173,6 @@ impl ObjArch for ObjArchPpc {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_NONE => Cow::Borrowed("R_PPC_NONE"), // We use this for fake pool relocs
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
@@ -194,22 +188,26 @@ impl ObjArch for ObjArchPpc {
}
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String);
}
let op = Opcode::from(instruction.op as u8);
if let Some(ty) = guess_data_type_from_load_store_inst_op(op) {
Some(ty)
} else if op == Opcode::Addi {
// Assume that any addi instruction that references a local symbol is loading a string.
// This hack is not ideal and results in tons of false positives where it will show
// garbage strings (e.g. misinterpreting arrays, float literals, etc).
// But not all strings are in the @stringBase pool, so the condition above that checks
// the target symbol name would miss some.
Some(DataType::String)
} else {
None
match Opcode::from(instruction.op as u8) {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
@@ -383,196 +381,3 @@ fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
}
fn guess_data_type_from_load_store_inst_op(inst_op: Opcode) -> Option<DataType> {
match inst_op {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
// Given an instruction, determine if it could accessing data at the address in a register.
// If so, return the offset added to the register's address, the register containing that address,
// and (optionally) which destination register the address is being copied into.
fn get_offset_and_addr_gpr_for_possible_pool_reference(
opcode: Opcode,
simplified: &ParsedIns,
) -> Option<(i16, GPR, Option<GPR>)> {
let args = &simplified.args;
if guess_data_type_from_load_store_inst_op(opcode).is_some() {
match (args[1], args[2]) {
(Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => {
// e.g. lwz. Immediate offset.
Some((offset.0, addr_src_gpr, None))
}
(Argument::GPR(addr_src_gpr), Argument::GPR(_offset_gpr)) => {
// e.g. lwzx. The offset is in a register and was likely calculated from an index.
// Treat the offset as being 0 in this case to show the first element of the array.
// It may be possible to show all elements by figuring out the stride of the array
// from the calculations performed on the index before it's put into offset_gpr, but
// this would be much more complicated, so it's not currently done.
Some((0, addr_src_gpr, None))
}
_ => None,
}
} else {
// If it's not a load/store instruction, there's two more possibilities we need to handle.
// 1. It could be loading a pointer to a string.
// 2. It could be moving the relocation address plus an offset into a different register to
// load from later.
// If either of these match, we also want to return the destination register that the
// address is being copied into so that we can detect any future references to that new
// register as well.
match (opcode, args[0], args[1], args[2]) {
(
Opcode::Addi,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Simm(simm),
) => Some((simm.0, addr_src_gpr, Some(addr_dst_gpr))),
(
Opcode::Or,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::None,
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))), // `mr` or `mr.`
_ => None,
}
}
}
// We create a fake relocation for an instruction, vaguely simulating what the actual relocation
// might have looked like if it wasn't pooled. This is so minimal changes are needed to display
// pooled accesses vs non-pooled accesses. We set the relocation type to R_PPC_NONE to indicate that
// there isn't really a relocation here, as copying the pool relocation's type wouldn't make sense.
// Also, if this instruction is accessing the middle of a symbol instead of the start, we add an
// addend to indicate that.
fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &ObjReloc) -> Option<ObjReloc> {
let offset_from_pool = pool_reloc.addend + offset as i64;
let target_address = pool_reloc.target.address.checked_add_signed(offset_from_pool)?;
let orig_section_index = pool_reloc.target.orig_section_index?;
// We also need to create a fake target symbol to go inside our fake relocation.
// This is because we don't have access to list of all symbols in this section, so we can't find
// the real symbol yet. Instead we make a placeholder that has the correct `orig_section_index`
// and `address` fields, and then later on when this information is displayed to the user, we
// can find the real symbol by searching through the object's section's symbols for one that
// contains this address.
let fake_target_symbol = ObjSymbol {
name: "".to_string(),
demangled_name: None,
address: target_address,
section_address: 0,
size: 0,
size_known: false,
kind: Default::default(),
flags: Default::default(),
orig_section_index: Some(orig_section_index),
virtual_address: None,
original_index: None,
bytes: vec![],
};
// The addend is also fake because we don't know yet if the `target_address` here is the exact
// start of the symbol or if it's in the middle of it.
let fake_addend = 0;
Some(ObjReloc {
flags: RelocationFlags::Elf { r_type: elf::R_PPC_NONE },
address: cur_addr as u64,
target: fake_target_symbol,
addend: fake_addend,
})
}
// Searches through all instructions in a function, determining which registers have the addresses
// of pooled data relocations in them, finding which instructions load data from those addresses,
// and constructing a mapping of the address of that instruction to a "fake pool relocation" that
// simulates what that instruction's relocation would look like if data hadn't been pooled.
// Limitations: This method currently only goes through the instructions in a function in linear
// order, from start to finish. It does *not* follow any branches. This means that it could have
// false positives or false negatives in determining which relocation is currently loaded in which
// register at any given point in the function, as control flow is not respected.
// There are currently no known examples of this method producing inaccurate results in reality, but
// if examples are found, it may be possible to update this method to also follow all branches so
// that it produces more accurate results.
fn generate_fake_pool_reloc_for_addr_mapping(
address: u64,
code: &[u8],
relocations: &[ObjReloc],
) -> HashMap<u32, ObjReloc> {
let mut active_pool_relocs = HashMap::new();
let mut pool_reloc_for_addr = HashMap::new();
for (cur_addr, ins) in InsIter::new(code, address as u32) {
let simplified = ins.simplified();
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc {
// This instruction has a real relocation, so it may be a pool load we want to keep
// track of.
let args = &simplified.args;
match (ins.op, args[0], args[1], args[2]) {
(
Opcode::Addi,
Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr),
Argument::Simm(_simm),
) => {
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `addi`
}
(
Opcode::Ori,
Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr),
Argument::Uimm(_uimm),
) => {
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `ori`
}
(Opcode::B, _, _, _) => {
if simplified.mnemonic == "bl" {
// When encountering a function call, clear any active pool relocations from
// the volatile registers (r0, r3-r12), but not the nonvolatile registers.
active_pool_relocs.remove(&0);
for gpr in 3..12 {
active_pool_relocs.remove(&gpr);
}
}
}
_ => {}
}
} else if let Some((offset, addr_src_gpr, addr_dst_gpr)) =
get_offset_and_addr_gpr_for_possible_pool_reference(ins.op, &simplified)
{
// This instruction doesn't have a real relocation, so it may be a reference to one of
// the already-loaded pools.
if let Some(pool_reloc) = active_pool_relocs.get(&addr_src_gpr.0) {
if let Some(fake_pool_reloc) = make_fake_pool_reloc(offset, cur_addr, pool_reloc) {
pool_reloc_for_addr.insert(cur_addr, fake_pool_reloc);
}
if let Some(addr_dst_gpr) = addr_dst_gpr {
// If the address of the pool relocation got copied into another register, we
// need to keep track of it in that register too as future instructions may
// reference the symbol indirectly via this new register, instead of the
// register the symbol's address was originally loaded into.
// For example, the start of the function might `lis` + `addi` the start of the
// ...data pool into r25, and then later the start of a loop will `addi` r25
// with the offset within the .data section of an array variable into r21.
// Then the body of the loop will `lwzx` one of the array elements from r21.
let mut new_reloc = pool_reloc.clone();
new_reloc.addend += offset as i64;
active_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
}
}
}
}
pool_reloc_for_addr
}
-106
View File
@@ -1,106 +0,0 @@
pub mod watcher;
use std::{
path::{Path, PathBuf},
process::Command,
};
pub struct BuildStatus {
pub success: bool,
pub cmdline: String,
pub stdout: String,
pub stderr: String,
}
impl Default for BuildStatus {
fn default() -> Self {
BuildStatus {
success: true,
cmdline: String::new(),
stdout: String::new(),
stderr: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
pub fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
let Some(cwd) = &config.project_dir else {
return BuildStatus {
success: false,
stderr: "Missing project dir".to_string(),
..Default::default()
};
};
let make = config.custom_make.as_deref().unwrap_or("make");
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
command.current_dir(cwd).args(make_args).arg(arg);
command
};
#[cfg(windows)]
let mut command = {
use std::os::windows::process::CommandExt;
use path_slash::PathExt;
let mut command = if config.selected_wsl_distro.is_some() {
Command::new("wsl")
} else {
Command::new(make)
};
if let Some(distro) = &config.selected_wsl_distro {
// Strip distro root prefix \\wsl.localhost\{distro}
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
Err(_) => cwd.to_string_lossy().to_string(),
};
command
.arg("--cd")
.arg(cwd)
.arg("-d")
.arg(distro)
.arg("--")
.arg(make)
.args(make_args)
.arg(arg.to_slash_lossy().as_ref());
} else {
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
}
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
for arg in command.get_args() {
cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
}
let output = match command.output() {
Ok(output) => output,
Err(e) => {
return BuildStatus {
success: false,
cmdline,
stdout: Default::default(),
stderr: e.to_string(),
};
}
};
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
let stdout = String::from_utf8(output.stdout)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
}
-75
View File
@@ -1,75 +0,0 @@
use std::{
fs,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
task::Waker,
time::Duration,
};
use globset::GlobSet;
use notify::RecursiveMode;
use notify_debouncer_full::{new_debouncer_opt, DebounceEventResult};
pub type Watcher = notify_debouncer_full::Debouncer<
notify::RecommendedWatcher,
notify_debouncer_full::RecommendedCache,
>;
pub struct WatcherState {
pub config_path: Option<PathBuf>,
pub left_obj_path: Option<PathBuf>,
pub right_obj_path: Option<PathBuf>,
pub patterns: GlobSet,
}
pub fn create_watcher(
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
waker: Waker,
) -> notify::Result<Watcher> {
let base_dir = fs::canonicalize(project_dir)?;
let base_dir_clone = base_dir.clone();
let timeout = Duration::from_millis(200);
let config = notify::Config::default().with_poll_interval(Duration::from_secs(2));
let mut debouncer = new_debouncer_opt(
timeout,
None,
move |result: DebounceEventResult| match result {
Ok(events) => {
let mut any_match = false;
for event in events.iter() {
if !matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
continue;
}
for path in &event.paths {
let Ok(path) = path.strip_prefix(&base_dir_clone) else {
continue;
};
if patterns.is_match(path) {
// log::info!("File modified: {}", path.display());
any_match = true;
}
}
}
if any_match {
modified.store(true, Ordering::Relaxed);
waker.wake_by_ref();
}
}
Err(errors) => errors.iter().for_each(|e| log::error!("Watch error: {e:?}")),
},
notify_debouncer_full::RecommendedCache::new(),
config,
)?;
debouncer.watch(base_dir, RecursiveMode::Recursive)?;
Ok(debouncer)
}
-4
View File
@@ -176,10 +176,6 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
];
pub fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,
+3 -32
View File
@@ -9,7 +9,7 @@ use crate::{
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
ObjSymbolDiff,
},
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSection, ObjSymbol, ObjSymbolFlags, SymbolRef},
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbolFlags, SymbolRef},
};
pub fn process_code_symbol(
@@ -21,30 +21,14 @@ pub fn process_code_symbol(
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let mut res = obj.arch.process_code(
obj.arch.process_code(
symbol.address,
code,
section.orig_index,
&section.relocations,
&section.line_info,
config,
)?;
for inst in res.insts.iter_mut() {
if let Some(reloc) = &mut inst.reloc {
if reloc.target.size == 0 && reloc.target.name.is_empty() {
// Fake target symbol we added as a placeholder. We need to find the real one.
if let Some(real_target) =
find_symbol_matching_fake_symbol_in_sections(&reloc.target, &obj.sections)
{
reloc.addend = (reloc.target.address - real_target.address) as i64;
reloc.target = real_target;
}
}
}
}
Ok(res)
)
}
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
@@ -385,16 +369,3 @@ fn compare_ins(
}
Ok(result)
}
fn find_symbol_matching_fake_symbol_in_sections(
fake_symbol: &ObjSymbol,
sections: &[ObjSection],
) -> Option<ObjSymbol> {
let orig_section_index = fake_symbol.orig_section_index?;
let section = sections.iter().find(|s| s.orig_index == orig_section_index)?;
let real_symbol = section
.symbols
.iter()
.find(|s| s.size > 0 && (s.address..s.address + s.size).contains(&fake_symbol.address))?;
Some(real_symbol.clone())
}
-50
View File
@@ -1,50 +0,0 @@
use std::{sync::mpsc::Receiver, task::Waker};
use anyhow::{Context, Result};
use self_update::{
cargo_crate_version,
update::{Release, ReleaseUpdate},
};
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
pub struct CheckUpdateConfig {
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
pub bin_names: Vec<String>,
}
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: Option<String>,
}
fn run_check_update(
context: &JobContext,
cancel: Receiver<()>,
config: CheckUpdateConfig,
) -> Result<Box<CheckUpdateResult>> {
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = (config.build_updater)().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
// Find the binary name in the release assets
let mut found_binary = None;
for bin_name in &config.bin_names {
if latest_release.assets.iter().any(|a| &a.name == bin_name) {
found_binary = Some(bin_name.clone());
break;
}
}
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState {
start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| {
run_check_update(&context, cancel, config)
.map(|result| JobResult::CheckUpdate(Some(result)))
})
}
-199
View File
@@ -1,199 +0,0 @@
use std::{path::PathBuf, sync::mpsc::Receiver, task::Waker};
use anyhow::{anyhow, Error, Result};
use time::OffsetDateTime;
use crate::{
build::{run_make, BuildConfig, BuildStatus},
config::SymbolMappings,
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
obj::{read, ObjInfo},
};
pub struct ObjDiffConfig {
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub diff_obj_config: DiffObjConfig,
pub symbol_mappings: SymbolMappings,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<(ObjInfo, ObjDiff)>,
pub second_obj: Option<(ObjInfo, ObjDiff)>,
pub time: OffsetDateTime,
}
fn run_build(
context: &JobContext,
cancel: Receiver<()>,
mut config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
// Use the per-object symbol mappings, we don't set mappings globally
config.diff_obj_config.symbol_mappings = MappingConfig {
mappings: config.symbol_mappings,
selecting_left: config.selecting_left,
selecting_right: config.selecting_right,
};
let mut target_path_rel = None;
let mut base_path_rel = None;
if config.build_target || config.build_base {
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing project dir"))?;
if let Some(target_path) = &config.target_path {
target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Target path '{}' doesn't begin with '{}'",
target_path.display(),
project_dir.display()
)
})?);
}
if let Some(base_path) = &config.base_path {
base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?);
};
}
let mut total = 1;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
if config.target_path.is_some() {
total += 1;
}
if config.base_path.is_some() {
total += 1;
}
let mut step_idx = 0;
let mut first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let mut second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};
let time = OffsetDateTime::now_utc();
let first_obj = match &config.target_path {
Some(target_path) if first_status.success => {
update_status(
context,
format!("Loading target {}", target_path.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(target_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
let second_obj = match &config.base_path {
Some(base_path) if second_status.success => {
update_status(
context,
format!("Loading base {}", base_path.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(base_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
second_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", base_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult {
first_status,
second_status,
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
time,
}))
}
pub fn start_build(waker: Waker, config: ObjDiffConfig) -> JobState {
start_job(waker, "Build", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}
-4
View File
@@ -2,14 +2,10 @@
pub mod arch;
#[cfg(feature = "bindings")]
pub mod bindings;
#[cfg(feature = "build")]
pub mod build;
#[cfg(feature = "config")]
pub mod config;
#[cfg(feature = "any-arch")]
pub mod diff;
#[cfg(feature = "build")]
pub mod jobs;
#[cfg(feature = "any-arch")]
pub mod obj;
#[cfg(feature = "any-arch")]
+14 -1
View File
@@ -29,7 +29,7 @@ bytes = "1.9"
cfg-if = "1.0"
const_format = "0.2"
cwdemangle = "1.0"
cwextab = "1.0"
cwextab = "1.0.2"
dirs = "5.0"
egui = "0.29"
egui_extras = "0.29"
@@ -38,6 +38,7 @@ float-ord = "0.3"
font-kit = "0.14"
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"
@@ -50,6 +51,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] }
tempfile = "3.14"
time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui
@@ -74,7 +76,18 @@ features = [
optional = true
default-features = false
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.41"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2"
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
+128 -40
View File
@@ -11,22 +11,25 @@ use std::{
};
use filetime::FileTime;
use globset::Glob;
use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher};
use objdiff_core::{
build::watcher::{create_watcher, Watcher},
config::{
build_globset, default_watch_patterns, save_project_config, ProjectConfig,
ProjectConfigInfo, ProjectObject, ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
},
diff::DiffObjConfig,
jobs::{Job, JobQueue, JobResult},
};
use time::UtcOffset;
use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{load_project_config, ProjectObjectNode},
jobs::{create_objdiff_config, egui_waker, start_build},
hotkeys,
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
},
views::{
appearance::{appearance_window, Appearance},
config::{
@@ -119,6 +122,11 @@ impl From<&ProjectObject> for ObjectConfig {
#[inline]
fn bool_true() -> bool { true }
#[inline]
fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
pub struct AppState {
pub config: AppConfig,
pub objects: Vec<ProjectObject>,
@@ -392,7 +400,7 @@ pub struct App {
view_state: ViewState,
state: AppStateRef,
modified: Arc<AtomicBool>,
watcher: Option<Watcher>,
watcher: Option<notify::RecommendedWatcher>,
app_path: Option<PathBuf>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
should_relaunch: bool,
@@ -467,17 +475,53 @@ impl App {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
jobs.collect_results();
jobs.results.retain(|result| match result {
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path.clone());
self.should_relaunch = true;
let mut results = vec![];
for (job, result) in jobs.iter_finished() {
match result {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.context.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
self.should_relaunch = true;
}
}
_ => results.push(result),
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.context.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.context.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: Some(err),
}));
}
}
false
}
_ => true,
});
}
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.state);
config_state.pre_update(jobs, &self.state);
debug_assert!(jobs.results.is_empty());
@@ -533,7 +577,7 @@ impl App {
match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(self.modified.clone(), project_dir, globset, egui_waker(ctx))
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
}) {
Ok(watcher) => self.watcher = Some(watcher),
@@ -580,15 +624,15 @@ impl App {
&& state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff)
{
start_build(ctx, jobs, create_objdiff_config(state));
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
state.queue_build = false;
state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = create_objdiff_config(state);
let mut diff_config = ObjDiffConfig::from_state(state);
// Don't build, just reload the current files
diff_config.build_base = false;
diff_config.build_target = false;
start_build(ctx, jobs, diff_config);
jobs.push(start_build(ctx, diff_config));
state.queue_reload = false;
}
@@ -651,13 +695,13 @@ impl eframe::App for App {
*show_side_panel = !*show_side_panel;
}
ui.separator();
ui.menu_button("File", |ui| {
ui.menu_button(hotkeys::button_alt_text(ui, "_File"), |ui| {
#[cfg(debug_assertions)]
if ui.button("Debug…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Debug…")).clicked() {
*show_debug = !*show_debug;
ui.close_menu();
}
if ui.button("Project…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Project…")).clicked() {
*show_project_config = !*show_project_config;
ui.close_menu();
}
@@ -666,10 +710,11 @@ impl eframe::App for App {
} else {
vec![]
};
let recent_projects_text = hotkeys::button_alt_text(ui, "_Recent Projects…");
if recent_projects.is_empty() {
ui.add_enabled(false, egui::Button::new("Recent projects…"));
ui.add_enabled(false, egui::Button::new(recent_projects_text));
} else {
ui.menu_button("Recent Projects…", |ui| {
ui.menu_button(recent_projects_text, |ui| {
if ui.button("Clear").clicked() {
state.write().unwrap().config.recent_projects.clear();
};
@@ -682,36 +727,39 @@ impl eframe::App for App {
}
});
}
if ui.button("Appearance…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Appearance…")).clicked() {
*show_appearance_config = !*show_appearance_config;
ui.close_menu();
}
if ui.button("Graphics…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Graphics…")).clicked() {
*show_graphics = !*show_graphics;
ui.close_menu();
}
if ui.button("Quit").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Quit")).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle…").clicked() {
ui.menu_button(hotkeys::button_alt_text(ui, "_Tools"), |ui| {
if ui.button(hotkeys::button_alt_text(ui, "_Demangle…")).clicked() {
*show_demangle = !*show_demangle;
ui.close_menu();
}
if ui.button("Rlwinm Decoder…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Rlwinm Decoder…")).clicked() {
*show_rlwinm_decode = !*show_rlwinm_decode;
ui.close_menu();
}
});
ui.menu_button("Diff Options", |ui| {
if ui.button("Arch Settings…").clicked() {
ui.menu_button(hotkeys::button_alt_text(ui, "_Diff Options"), |ui| {
if ui.button(hotkeys::button_alt_text(ui, "_Arch Settings…")).clicked() {
*show_arch_config = !*show_arch_config;
ui.close_menu();
}
let mut state = state.write().unwrap();
let response = ui
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
.checkbox(
&mut state.config.rebuild_on_changes,
hotkeys::button_alt_text(ui, "_Rebuild on changes"),
)
.on_hover_text("Automatically re-run the build & diff when files change.");
if response.changed() {
state.watcher_change = true;
@@ -720,18 +768,21 @@ impl eframe::App for App {
!diff_state.symbol_state.disable_reverse_fn_order,
egui::Checkbox::new(
&mut diff_state.symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
hotkeys::button_alt_text(
ui,
"Reverse function order (-inline _deferred)",
),
),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
ui.checkbox(
&mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols",
hotkeys::button_alt_text(ui, "Show _hidden symbols"),
);
if ui
.checkbox(
&mut state.config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs",
hotkeys::button_alt_text(ui, "Rela_x relocation diffs"),
)
.on_hover_text(
"Ignores differences in relocation targets. (Address, name, etc)",
@@ -743,7 +794,7 @@ impl eframe::App for App {
if ui
.checkbox(
&mut state.config.diff_obj_config.space_between_args,
"Space between args",
hotkeys::button_alt_text(ui, "_Space between args"),
)
.changed()
{
@@ -752,14 +803,17 @@ impl eframe::App for App {
if ui
.checkbox(
&mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections",
hotkeys::button_alt_text(ui, "Combine _data sections"),
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
state.queue_reload = true;
}
if ui.button("Clear custom symbol mappings").clicked() {
if ui
.button(hotkeys::button_alt_text(ui, "_Clear custom symbol mappings"))
.clicked()
{
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
state.queue_reload = true;
@@ -815,6 +869,40 @@ impl eframe::App for App {
}
}
fn create_watcher(
ctx: egui::Context,
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> {
let base_dir = project_dir.to_owned();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
for path in &event.paths {
let Ok(path) = path.strip_prefix(&base_dir) else {
continue;
};
if patterns.is_match(path) {
log::info!("File modified: {}", path.display());
modified.store(true, Ordering::Relaxed);
ctx.request_repaint();
}
}
}
}
Err(e) => log::error!("watch error: {e:?}"),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}
#[inline]
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
if let Ok(metadata) = fs::metadata(path) {
+121 -1
View File
@@ -1,5 +1,6 @@
use egui::{
style::ScrollAnimation, vec2, Context, Key, KeyboardShortcut, Modifiers, PointerButton,
style::ScrollAnimation, text::LayoutJob, vec2, Align, Context, FontSelection, Key,
KeyboardShortcut, Modifiers, PointerButton, RichText, Ui, WidgetText,
};
fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) }
@@ -106,3 +107,122 @@ const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::
pub fn consume_change_base_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT))
}
fn shortcut_key(text: &str) -> (usize, char, Key) {
let i = text.chars().position(|c| c == '_').expect("No underscore in text");
let key = text.chars().nth(i + 1).expect("No character after underscore");
let shortcut_key = match key {
'a' | 'A' => Key::A,
'b' | 'B' => Key::B,
'c' | 'C' => Key::C,
'd' | 'D' => Key::D,
'e' | 'E' => Key::E,
'f' | 'F' => Key::F,
'g' | 'G' => Key::G,
'h' | 'H' => Key::H,
'i' | 'I' => Key::I,
'j' | 'J' => Key::J,
'k' | 'K' => Key::K,
'l' | 'L' => Key::L,
'm' | 'M' => Key::M,
'n' | 'N' => Key::N,
'o' | 'O' => Key::O,
'p' | 'P' => Key::P,
'q' | 'Q' => Key::Q,
'r' | 'R' => Key::R,
's' | 'S' => Key::S,
't' | 'T' => Key::T,
'u' | 'U' => Key::U,
'v' | 'V' => Key::V,
'w' | 'W' => Key::W,
'x' | 'X' => Key::X,
'y' | 'Y' => Key::Y,
'z' | 'Z' => Key::Z,
_ => panic!("Invalid key {}", key),
};
(i, key, shortcut_key)
}
fn alt_text_ui(ui: &Ui, text: &str, i: usize, key: char, interactive: bool) -> WidgetText {
let color = if interactive {
ui.visuals().widgets.inactive.text_color()
} else {
ui.visuals().widgets.noninteractive.text_color()
};
let mut job = LayoutJob::default();
if i > 0 {
let text = &text[..i];
RichText::new(text).color(color).append_to(
&mut job,
ui.style(),
FontSelection::Default,
Align::Center,
);
}
let mut rt = RichText::new(key).color(color);
if ui.input(|i| i.modifiers.alt) {
rt = rt.underline();
}
rt.append_to(&mut job, ui.style(), FontSelection::Default, Align::Center);
if i < text.len() - 1 {
let text = &text[i + 2..];
RichText::new(text).color(color).append_to(
&mut job,
ui.style(),
FontSelection::Default,
Align::Center,
);
}
job.into()
}
pub fn button_alt_text(ui: &Ui, text: &str) -> WidgetText {
let (n, c, key) = shortcut_key(text);
let result = alt_text_ui(ui, text, n, c, true);
if ui.input_mut(|i| check_alt_key(i, c, key)) {
let btn_id = ui.next_auto_id();
ui.memory_mut(|m| m.request_focus(btn_id));
ui.input_mut(|i| {
i.events.push(egui::Event::Key {
key: Key::Enter,
physical_key: None,
pressed: true,
repeat: false,
modifiers: Default::default(),
})
});
}
result
}
pub fn alt_text(ui: &Ui, text: &str, enter: bool) -> WidgetText {
let (n, c, key) = shortcut_key(text);
let result = alt_text_ui(ui, text, n, c, false);
if ui.input_mut(|i| check_alt_key(i, c, key)) {
let btn_id = ui.next_auto_id();
ui.memory_mut(|m| m.request_focus(btn_id));
if enter {
ui.input_mut(|i| {
i.events.push(egui::Event::Key {
key: Key::Enter,
physical_key: None,
pressed: true,
repeat: false,
modifiers: Default::default(),
})
});
}
}
result
}
fn check_alt_key(i: &mut egui::InputState, c: char, key: Key) -> bool {
if i.consume_key(Modifiers::ALT, key) {
// Remove any text input events that match the key
let cs = c.to_string();
i.events.retain(|e| !matches!(e, egui::Event::Text(t) if *t == cs));
true
} else {
false
}
}

Some files were not shown because too many files have changed in this diff Show More