Add CLI args to objdiff-gui (incl. --project-dir/-p)

Resolves #41
Resolves #211
This commit is contained in:
Luke Street 2025-08-15 15:25:55 -06:00
parent 247d6da94b
commit b21892be31
5 changed files with 205 additions and 21 deletions

1
Cargo.lock generated
View File

@ -3517,6 +3517,7 @@ name = "objdiff-gui"
version = "3.0.0-beta.14" version = "3.0.0-beta.14"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argp",
"cfg-if", "cfg-if",
"const_format", "const_format",
"cwdemangle", "cwdemangle",

View File

@ -25,6 +25,7 @@ wsl = []
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
argp = "0.4"
cfg-if = "1.0" cfg-if = "1.0"
const_format = "0.2" const_format = "0.2"
cwdemangle = "1.0" cwdemangle = "1.0"

View File

@ -431,6 +431,7 @@ impl App {
app_path: Option<PathBuf>, app_path: Option<PathBuf>,
graphics_config: GraphicsConfig, graphics_config: GraphicsConfig,
graphics_config_path: Option<PathBuf>, graphics_config_path: Option<PathBuf>,
project_dir: Option<Utf8PlatformPathBuf>,
) -> Self { ) -> Self {
// Load previous app state (if any). // Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work. // Note that you must enable the `persistence` feature for this to work.
@ -440,18 +441,26 @@ impl App {
app.appearance = appearance; app.appearance = appearance;
} }
if let Some(config) = deserialize_config(storage) { if let Some(config) = deserialize_config(storage) {
let mut state = AppState { config, ..Default::default() }; let 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;
app.state = Arc::new(RwLock::new(state)); 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.init_fonts(&cc.egui_ctx);
app.appearance.utc_offset = utc_offset; app.appearance.utc_offset = utc_offset;
app.app_path = app_path; app.app_path = app_path;

View File

@ -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>(T)
where T: FromArgs;
impl<T> TopLevelCommand for ArgsOrVersion<T> where T: FromArgs {}
impl<T> FromArgs for ArgsOrVersion<T>
where T: FromArgs
{
fn _from_args(
command_name: &[&str],
args: &[&OsStr],
parent: Option<&mut dyn ParseGlobalOptions>,
) -> Result<Self, EarlyExit> {
/// 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 processs `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>() -> T
where T: TopLevelCommand {
argp::parse_args_or_exit::<ArgsOrVersion<T>>(argp::DEFAULT).0
}

View File

@ -3,6 +3,7 @@
mod app; mod app;
mod app_config; mod app_config;
mod argp_version;
mod config; mod config;
mod fonts; mod fonts;
mod hotkeys; mod hotkeys;
@ -11,19 +12,83 @@ mod update;
mod views; mod views;
use std::{ use std::{
ffi::OsStr,
fmt::Display,
path::PathBuf, path::PathBuf,
process::ExitCode, process::ExitCode,
rc::Rc, rc::Rc,
str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use anyhow::{Result, ensure}; use anyhow::{Result, ensure};
use argp::{FromArgValue, FromArgs};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use objdiff_core::config::path::check_path_buf;
use time::UtcOffset; 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}; 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<Self, Self::Err> {
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<Self, String> {
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<LogLevel>,
#[argp(option, short = 'p')]
/// Path to the project directory.
project_dir: Option<PathBuf>,
/// Print version information and exit.
#[argp(switch, short = 'V')]
version: bool,
}
fn load_icon() -> Result<egui::IconData> { fn load_icon() -> Result<egui::IconData> {
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").as_ref()); let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").as_ref());
let mut reader = decoder.read_info()?; let mut reader = decoder.read_info()?;
@ -38,23 +103,63 @@ fn load_icon() -> Result<egui::IconData> {
const APP_NAME: &str = "objdiff"; const APP_NAME: &str = "objdiff";
fn main() -> ExitCode { fn main() -> ExitCode {
// Log to stdout (if you run with `RUST_LOG=debug`). let args: TopLevel = argp_version::from_env();
tracing_subscriber::fmt() let builder = tracing_subscriber::fmt();
.with_env_filter( if let Some(level) = args.log_level {
EnvFilter::builder() builder
// Default to info level .with_max_level(match level {
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) LogLevel::Error => LevelFilter::ERROR,
.from_env_lossy() LogLevel::Warn => LevelFilter::WARN,
// This module is noisy at info level LogLevel::Info => LevelFilter::INFO,
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()), LogLevel::Debug => LevelFilter::DEBUG,
) LogLevel::Trace => LevelFilter::TRACE,
.init(); })
.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, // Because localtime_r is unsound in multithreaded apps,
// we must call this before initializing eframe. // we must call this before initializing eframe.
// https://github.com/time-rs/time/issues/293 // https://github.com/time-rs/time/issues/293
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); 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 app_path = std::env::current_exe().ok();
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None)); let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let mut native_options = eframe::NativeOptions { let mut native_options = eframe::NativeOptions {
@ -113,6 +218,7 @@ fn main() -> ExitCode {
app_path.clone(), app_path.clone(),
graphics_config.clone(), graphics_config.clone(),
graphics_config_path.clone(), graphics_config_path.clone(),
project_dir.clone(),
) { ) {
eframe_error = Some(e); eframe_error = Some(e);
} }
@ -139,6 +245,7 @@ fn main() -> ExitCode {
app_path.clone(), app_path.clone(),
graphics_config.clone(), graphics_config.clone(),
graphics_config_path.clone(), graphics_config_path.clone(),
project_dir.clone(),
) { ) {
eframe_error = Some(e); eframe_error = Some(e);
} else { } else {
@ -161,6 +268,7 @@ fn main() -> ExitCode {
app_path, app_path,
graphics_config, graphics_config,
graphics_config_path, graphics_config_path,
project_dir,
) { ) {
eframe_error = Some(e); eframe_error = Some(e);
} else { } else {
@ -204,6 +312,7 @@ fn run_eframe(
app_path: Option<PathBuf>, app_path: Option<PathBuf>,
graphics_config: GraphicsConfig, graphics_config: GraphicsConfig,
graphics_config_path: Option<PathBuf>, graphics_config_path: Option<PathBuf>,
project_dir: Option<Utf8PlatformPathBuf>,
) -> Result<(), eframe::Error> { ) -> Result<(), eframe::Error> {
eframe::run_native( eframe::run_native(
APP_NAME, APP_NAME,
@ -216,6 +325,7 @@ fn run_eframe(
app_path, app_path,
graphics_config, graphics_config,
graphics_config_path, graphics_config_path,
project_dir,
))) )))
}), }),
) )