diff --git a/Cargo.lock b/Cargo.lock index 59fc19c..aebf0d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3517,6 +3517,7 @@ name = "objdiff-gui" version = "3.0.0-beta.14" dependencies = [ "anyhow", + "argp", "cfg-if", "const_format", "cwdemangle", diff --git a/objdiff-gui/Cargo.toml b/objdiff-gui/Cargo.toml index 63d05bd..b487983 100644 --- a/objdiff-gui/Cargo.toml +++ b/objdiff-gui/Cargo.toml @@ -25,6 +25,7 @@ wsl = [] [dependencies] anyhow = "1.0" +argp = "0.4" cfg-if = "1.0" const_format = "0.2" cwdemangle = "1.0" diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index b591f33..880df3c 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -431,6 +431,7 @@ impl App { app_path: Option, graphics_config: GraphicsConfig, graphics_config_path: Option, + project_dir: Option, ) -> Self { // Load previous app state (if any). // Note that you must enable the `persistence` feature for this to work. @@ -440,18 +441,26 @@ impl App { app.appearance = appearance; } if let Some(config) = deserialize_config(storage) { - let mut state = AppState { config, ..Default::default() }; - if state.config.project_dir.is_some() { - state.config_change = true; - state.watcher_change = true; - } - if state.config.selected_obj.is_some() { - state.queue_build = true; - } - app.view_state.config_state.queue_check_update = state.config.auto_update_check; + let state = AppState { config, ..Default::default() }; app.state = Arc::new(RwLock::new(state)); } } + { + let mut state = app.state.write().unwrap(); + if let Some(project_dir) = project_dir + && state.config.project_dir.as_ref().is_none_or(|p| *p != project_dir) + { + state.set_project_dir(project_dir); + } + if state.config.project_dir.is_some() { + state.config_change = true; + state.watcher_change = true; + } + if state.config.selected_obj.is_some() { + state.queue_build = true; + } + app.view_state.config_state.queue_check_update = state.config.auto_update_check; + } app.appearance.init_fonts(&cc.egui_ctx); app.appearance.utc_offset = utc_offset; app.app_path = app_path; diff --git a/objdiff-gui/src/argp_version.rs b/objdiff-gui/src/argp_version.rs new file mode 100644 index 0000000..d9dae71 --- /dev/null +++ b/objdiff-gui/src/argp_version.rs @@ -0,0 +1,63 @@ +// Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c +//! Extend `argp` to be better integrated with the `cargo` ecosystem +//! +//! For now, this only adds a --version/-V option which causes early-exit. +use std::ffi::OsStr; + +use argp::{EarlyExit, FromArgs, TopLevelCommand, parser::ParseGlobalOptions}; + +struct ArgsOrVersion(T) +where T: FromArgs; + +impl TopLevelCommand for ArgsOrVersion where T: FromArgs {} + +impl FromArgs for ArgsOrVersion +where T: FromArgs +{ + fn _from_args( + command_name: &[&str], + args: &[&OsStr], + parent: Option<&mut dyn ParseGlobalOptions>, + ) -> Result { + /// Also use argp for catching `--version`-only invocations + #[derive(FromArgs)] + struct Version { + /// Print version information and exit. + #[argp(switch, short = 'V')] + pub version: bool, + } + + match Version::from_args(command_name, args) { + Ok(v) => { + if v.version { + println!( + "{} {}", + command_name.first().unwrap_or(&""), + env!("CARGO_PKG_VERSION"), + ); + std::process::exit(0); + } else { + // Pass through empty arguments + T::_from_args(command_name, args, parent).map(Self) + } + } + Err(exit) => match exit { + EarlyExit::Help(_help) => { + // TODO: Chain help info from Version + // For now, we just put the switch on T as well + T::from_args(command_name, &["--help"]).map(Self) + } + EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self), + }, + } + } +} + +/// Create a `FromArgs` type from the current process’s `env::args`. +/// +/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested. +/// Error messages will be printed to stderr, and `--help` output to stdout. +pub fn from_env() -> T +where T: TopLevelCommand { + argp::parse_args_or_exit::>(argp::DEFAULT).0 +} diff --git a/objdiff-gui/src/main.rs b/objdiff-gui/src/main.rs index dbd1dc3..4e7253d 100644 --- a/objdiff-gui/src/main.rs +++ b/objdiff-gui/src/main.rs @@ -3,6 +3,7 @@ mod app; mod app_config; +mod argp_version; mod config; mod fonts; mod hotkeys; @@ -11,19 +12,83 @@ mod update; mod views; use std::{ + ffi::OsStr, + fmt::Display, path::PathBuf, process::ExitCode, rc::Rc, + str::FromStr, sync::{Arc, Mutex}, }; use anyhow::{Result, ensure}; +use argp::{FromArgValue, FromArgs}; use cfg_if::cfg_if; +use objdiff_core::config::path::check_path_buf; use time::UtcOffset; -use tracing_subscriber::EnvFilter; +use tracing_subscriber::{EnvFilter, filter::LevelFilter}; +use typed_path::Utf8PlatformPathBuf; use crate::views::graphics::{GraphicsBackend, GraphicsConfig, load_graphics_config}; +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum LogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl FromStr for LogLevel { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "error" => Self::Error, + "warn" => Self::Warn, + "info" => Self::Info, + "debug" => Self::Debug, + "trace" => Self::Trace, + _ => return Err(()), + }) + } +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + }) + } +} + +impl FromArgValue for LogLevel { + fn from_arg_value(value: &OsStr) -> Result { + String::from_arg_value(value) + .and_then(|s| Self::from_str(&s).map_err(|_| "Invalid log level".to_string())) + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// A local diffing tool for decompilation projects. +struct TopLevel { + #[argp(option, short = 'L')] + /// Minimum logging level. (Default: info) + /// Possible values: error, warn, info, debug, trace + log_level: Option, + #[argp(option, short = 'p')] + /// Path to the project directory. + project_dir: Option, + /// Print version information and exit. + #[argp(switch, short = 'V')] + version: bool, +} + fn load_icon() -> Result { let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").as_ref()); let mut reader = decoder.read_info()?; @@ -38,23 +103,63 @@ fn load_icon() -> Result { const APP_NAME: &str = "objdiff"; fn main() -> ExitCode { - // Log to stdout (if you run with `RUST_LOG=debug`). - tracing_subscriber::fmt() - .with_env_filter( - EnvFilter::builder() - // Default to info level - .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) - .from_env_lossy() - // This module is noisy at info level - .add_directive("wgpu_core::device::resource=warn".parse().unwrap()), - ) - .init(); + let args: TopLevel = argp_version::from_env(); + let builder = tracing_subscriber::fmt(); + if let Some(level) = args.log_level { + builder + .with_max_level(match level { + LogLevel::Error => LevelFilter::ERROR, + LogLevel::Warn => LevelFilter::WARN, + LogLevel::Info => LevelFilter::INFO, + LogLevel::Debug => LevelFilter::DEBUG, + LogLevel::Trace => LevelFilter::TRACE, + }) + .init(); + } else { + builder + .with_env_filter( + EnvFilter::builder() + // Default to info level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy() + // This module is noisy at info level + .add_directive("wgpu_core::device::resource=warn".parse().unwrap()), + ) + .init(); + } // Because localtime_r is unsound in multithreaded apps, // we must call this before initializing eframe. // https://github.com/time-rs/time/issues/293 let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); + // Resolve project directory if provided + let project_dir = if let Some(path) = args.project_dir { + match path.canonicalize() { + Ok(path) => { + // Ensure the path is a directory + if path.is_dir() { + match check_path_buf(path) { + Ok(path) => Some(path), + Err(e) => { + log::error!("Failed to convert project directory to UTF-8 path: {}", e); + None + } + } + } else { + log::error!("Project directory is not a directory: {}", path.display()); + None + } + } + Err(e) => { + log::error!("Failed to canonicalize project directory: {}", e); + None + } + } + } else { + None + }; + let app_path = std::env::current_exe().ok(); let exec_path: Rc>> = Rc::new(Mutex::new(None)); let mut native_options = eframe::NativeOptions { @@ -113,6 +218,7 @@ fn main() -> ExitCode { app_path.clone(), graphics_config.clone(), graphics_config_path.clone(), + project_dir.clone(), ) { eframe_error = Some(e); } @@ -139,6 +245,7 @@ fn main() -> ExitCode { app_path.clone(), graphics_config.clone(), graphics_config_path.clone(), + project_dir.clone(), ) { eframe_error = Some(e); } else { @@ -161,6 +268,7 @@ fn main() -> ExitCode { app_path, graphics_config, graphics_config_path, + project_dir, ) { eframe_error = Some(e); } else { @@ -204,6 +312,7 @@ fn run_eframe( app_path: Option, graphics_config: GraphicsConfig, graphics_config_path: Option, + project_dir: Option, ) -> Result<(), eframe::Error> { eframe::run_native( APP_NAME, @@ -216,6 +325,7 @@ fn run_eframe( app_path, graphics_config, graphics_config_path, + project_dir, ))) }), )