use std::{ io::stdout, mem, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, task::{Wake, Waker}, time::Duration, }; use anyhow::{Context, Result, anyhow, bail}; use argp::FromArgs; use crossterm::{ event, event::{DisableMouseCapture, EnableMouseCapture}, terminal::{ EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode, }, }; use objdiff_core::{ bindings::diff::DiffResult, build::{ BuildConfig, BuildStatus, watcher::{Watcher, create_watcher}, }, config::{ ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset, path::{check_path_buf, platform_path, platform_path_serde_option}, }, diff::{self, DiffObjConfig, MappingConfig, ObjectDiff}, jobs::{ Job, JobQueue, JobResult, objdiff::{ObjDiffConfig, start_build}, }, obj::{self, Object}, }; use ratatui::prelude::*; use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf}; use crate::{ cmd::apply_config_args, util::{ output::{OutputFormat, write_output}, term::crossterm_panic_handler, }, views::{EventControlFlow, EventResult, UiView, function_diff::FunctionDiffUi}, }; #[derive(FromArgs, PartialEq, Debug)] /// Diff two object files. (Interactive or one-shot mode) #[argp(subcommand, name = "diff")] pub struct Args { #[argp(option, short = '1', from_str_fn(platform_path))] /// Target object file target: Option, #[argp(option, short = '2', from_str_fn(platform_path))] /// Base object file base: Option, #[argp(option, short = 'p', from_str_fn(platform_path))] /// Project directory project: Option, #[argp(option, short = 'u')] /// Unit name within project unit: Option, #[argp(option, short = 'o', from_str_fn(platform_path))] /// Output file (one-shot mode) ("-" for stdout) output: Option, #[argp(option)] /// Output format (json, json-pretty, proto) (default: json) format: Option, #[argp(positional)] /// Function symbol to diff symbol: Option, #[argp(option, short = 'c')] /// Configuration property (key=value) config: Vec, #[argp(option, short = 'm')] /// Symbol mapping (target=base) mapping: Vec, #[argp(option)] /// Left symbol name for selection selecting_left: Option, #[argp(option)] /// Right symbol name for selection selecting_right: Option, } pub fn run(args: Args) -> Result<()> { let (target_path, base_path, project_config) = match (&args.target, &args.base, &args.project, &args.unit) { (Some(_), Some(_), None, None) | (Some(_), None, None, None) | (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None), (None, None, p, u) => { let project = match p { Some(project) => project.clone(), _ => check_path_buf( std::env::current_dir().context("Failed to get the current directory")?, ) .context("Current directory is not valid UTF-8")?, }; let Some((project_config, project_config_info)) = objdiff_core::config::try_project_config(project.as_ref()) else { bail!("Project config not found in {}", &project) }; let project_config = project_config.with_context(|| { format!("Reading project config {}", project_config_info.path.display()) })?; let target_obj_dir = project_config .target_dir .as_ref() .map(|p| project.join(p.with_platform_encoding())); let base_obj_dir = project_config .base_dir .as_ref() .map(|p| project.join(p.with_platform_encoding())); let objects = project_config .units .iter() .flatten() .map(|o| { ObjectConfig::new( o, &project, target_obj_dir.as_deref(), base_obj_dir.as_deref(), ) }) .collect::>(); let object = if let Some(u) = u { objects .iter() .find(|obj| obj.name == *u) .ok_or_else(|| anyhow!("Unit not found: {}", u))? } else if let Some(symbol_name) = &args.symbol { let mut idx = None; let mut count = 0usize; for (i, obj) in objects.iter().enumerate() { if obj .target_path .as_deref() .map(|o| obj::read::has_function(o.as_ref(), symbol_name)) .transpose()? .unwrap_or(false) { idx = Some(i); count += 1; if count > 1 { break; } } } match (count, idx) { (0, None) => bail!("Symbol not found: {}", symbol_name), (1, Some(i)) => &objects[i], (2.., Some(_)) => bail!( "Multiple instances of {} were found, try specifying a unit", symbol_name ), _ => unreachable!(), } } else { bail!("Must specify one of: symbol, project and unit, target and base objects") }; let target_path = object.target_path.clone(); let base_path = object.base_path.clone(); (target_path, base_path, Some(project_config)) } _ => bail!("Either target and base or project and unit must be specified"), }; if let Some(output) = &args.output { run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref()) } else { run_interactive(args, target_path, base_path, project_config) } } fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> { let mut diff_config = DiffObjConfig::default(); apply_config_args(&mut diff_config, &args.config)?; let mut mapping_config = MappingConfig { mappings: Default::default(), selecting_left: args.selecting_left.clone(), selecting_right: args.selecting_right.clone(), }; for mapping in &args.mapping { let (target, base) = mapping.split_once('=').context("--mapping expects \"target=base\"")?; mapping_config.mappings.insert(target.to_string(), base.to_string()); } Ok((diff_config, mapping_config)) } fn run_oneshot( args: &Args, output: &Utf8PlatformPath, target_path: Option<&Utf8PlatformPath>, base_path: Option<&Utf8PlatformPath>, ) -> Result<()> { let output_format = OutputFormat::from_option(args.format.as_deref())?; let (diff_config, mapping_config) = build_config_from_args(args)?; let target = target_path .map(|p| { obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p)) }) .transpose()?; let base = base_path .map(|p| { obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p)) }) .transpose()?; let result = diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?; let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d))); let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d))); write_output(&DiffResult::new(left, right), Some(output), output_format)?; Ok(()) } pub struct AppState { pub jobs: JobQueue, pub waker: Arc, pub project_dir: Option, pub project_config: Option, pub target_path: Option, pub base_path: Option, pub left_status: Option, pub right_status: Option, pub left_obj: Option<(Object, ObjectDiff)>, pub right_obj: Option<(Object, ObjectDiff)>, pub prev_obj: Option<(Object, ObjectDiff)>, pub reload_time: Option, pub time_format: Vec>, pub watcher: Option, pub modified: Arc, pub diff_obj_config: DiffObjConfig, pub mapping_config: MappingConfig, } fn create_objdiff_config(state: &AppState) -> ObjDiffConfig { ObjDiffConfig { build_config: BuildConfig { project_dir: state.project_dir.clone(), custom_make: state .project_config .as_ref() .and_then(|c| c.custom_make.as_ref()) .cloned(), custom_args: state .project_config .as_ref() .and_then(|c| c.custom_args.as_ref()) .cloned(), selected_wsl_distro: None, }, build_base: state.project_config.as_ref().is_some_and(|p| p.build_base.unwrap_or(true)), build_target: state .project_config .as_ref() .is_some_and(|p| p.build_target.unwrap_or(false)), target_path: state.target_path.clone(), base_path: state.base_path.clone(), diff_obj_config: state.diff_obj_config.clone(), mapping_config: state.mapping_config.clone(), } } /// The configuration for a single object file. #[derive(Default, Clone, serde::Deserialize, serde::Serialize)] pub struct ObjectConfig { pub name: String, #[serde(default, with = "platform_path_serde_option")] pub target_path: Option, #[serde(default, with = "platform_path_serde_option")] pub base_path: Option, pub metadata: ProjectObjectMetadata, pub complete: Option, } impl ObjectConfig { pub fn new( object: &ProjectObject, project_dir: &Utf8PlatformPath, target_obj_dir: Option<&Utf8PlatformPath>, base_obj_dir: Option<&Utf8PlatformPath>, ) -> Self { let target_path = if let (Some(target_obj_dir), Some(path), None) = (target_obj_dir, &object.path, &object.target_path) { Some(target_obj_dir.join(path.with_platform_encoding())) } else { object.target_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding())) }; let base_path = if let (Some(base_obj_dir), Some(path), None) = (base_obj_dir, &object.path, &object.base_path) { Some(base_obj_dir.join(path.with_platform_encoding())) } else { object.base_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding())) }; Self { name: object.name().to_string(), target_path, base_path, metadata: object.metadata.clone().unwrap_or_default(), complete: object.complete(), } } } impl AppState { fn reload(&mut self) -> Result<()> { let config = create_objdiff_config(self); self.jobs.push_once(Job::ObjDiff, || start_build(Waker::from(self.waker.clone()), config)); Ok(()) } fn check_jobs(&mut self) -> Result { let mut redraw = false; self.jobs.collect_results(); for result in mem::take(&mut self.jobs.results) { match result { JobResult::None => unreachable!("Unexpected JobResult::None"), JobResult::ObjDiff(result) => { let result = result.unwrap(); self.left_status = Some(result.first_status); self.right_status = Some(result.second_status); self.left_obj = result.first_obj; self.right_obj = result.second_obj; self.reload_time = Some(result.time); redraw = true; } JobResult::CheckUpdate(_) => todo!("CheckUpdate"), JobResult::Update(_) => todo!("Update"), JobResult::CreateScratch(_) => todo!("CreateScratch"), } } Ok(redraw) } } #[derive(Default)] pub struct TermWaker(pub AtomicBool); impl Wake for TermWaker { fn wake(self: Arc) { self.0.store(true, Ordering::Relaxed); } fn wake_by_ref(self: &Arc) { self.0.store(true, Ordering::Relaxed); } } fn run_interactive( args: Args, target_path: Option, base_path: Option, project_config: Option, ) -> Result<()> { let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]") .context("Failed to parse time format")?; let (diff_obj_config, mapping_config) = build_config_from_args(&args)?; let mut state = AppState { jobs: Default::default(), waker: Default::default(), project_dir: args.project.clone(), project_config, target_path, base_path, left_status: None, right_status: None, left_obj: None, right_obj: None, prev_obj: None, reload_time: None, time_format, watcher: None, modified: Default::default(), diff_obj_config, mapping_config, }; if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) { let watch_patterns = project_config.build_watch_patterns()?; state.watcher = Some(create_watcher( state.modified.clone(), project_dir.as_ref(), build_globset(&watch_patterns)?, Waker::from(state.waker.clone()), )?); } let mut view: Box = Box::new(FunctionDiffUi { symbol_name: symbol_name.clone(), ..Default::default() }); state.reload()?; crossterm_panic_handler(); enable_raw_mode()?; crossterm::queue!( stdout(), EnterAlternateScreen, EnableMouseCapture, SetTitle(format!("{} - objdiff", symbol_name)), )?; let backend = CrosstermBackend::new(stdout()); let mut terminal = Terminal::new(backend)?; let mut result = EventResult { redraw: true, ..Default::default() }; 'outer: loop { if result.redraw { terminal.draw(|f| { loop { result.redraw = false; view.draw(&state, f, &mut result); result.click_xy = None; if !result.redraw { break; } // Clear buffer on redraw f.buffer_mut().reset(); } })?; } loop { if event::poll(Duration::from_millis(100))? { match view.handle_event(&mut state, event::read()?) { EventControlFlow::Break => break 'outer, EventControlFlow::Continue(r) => result = r, EventControlFlow::Reload => { state.reload()?; result.redraw = true; } } break; } else if state.waker.0.swap(false, Ordering::Relaxed) { if state.modified.swap(false, Ordering::Relaxed) { state.reload()?; } result.redraw = true; break; } } if state.check_jobs()? { result.redraw = true; view.reload(&state)?; } } // Reset terminal disable_raw_mode()?; crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?; terminal.show_cursor()?; Ok(()) }