diff --git a/src/bin/mdo-open.rs b/src/bin/mdo-open.rs index df36de0..5cbb1f6 100644 --- a/src/bin/mdo-open.rs +++ b/src/bin/mdo-open.rs @@ -10,10 +10,13 @@ //! but break terminal usage (no stdout to the parent shell, output races the //! returning prompt under cmd/PowerShell). //! -//! Instead, `mdo-open[.exe]` is a tiny desktop wrapper that: -//! 1. Locates `mdo[.exe]` next to itself. -//! 2. Spawns it with `--open` and the rest of the args, using -//! `CREATE_NO_WINDOW` so the child never gets a console allocated. +//! Instead, `mdo-open[.exe]` is a tiny desktop wrapper that consumes the +//! shared handler invocation contract (`mdo_cli::handler`): +//! 1. Locates `mdo[.exe]` next to itself (the one sibling-binary +//! discovery function, so a missing sibling gets one clear message). +//! 2. Spawns it with the contract argv (`--open` plus the file args), +//! using `CREATE_NO_WINDOW` so the child never gets a console +//! allocated. //! 3. If launched directly with no file args, opens onboarding: Windows //! starts the terminal setup in Windows Terminal when available; Linux //! opens `mdo-setup` for native first-run onboarding. This step is @@ -48,8 +51,8 @@ use std::process::{Command, ExitCode}; #[cfg(target_os = "windows")] use std::io; -#[cfg(target_os = "windows")] -use std::path::Path; + +use mdo_cli::handler; #[cfg(target_os = "windows")] const MDO_BIN: &str = "mdo.exe"; @@ -60,12 +63,16 @@ const MDO_BIN: &str = "mdo"; const SETUP_BIN: &str = "mdo-setup"; fn main() -> ExitCode { - let mut exe_path = match env::current_exe() { + // A GUI-launched binary often has no console attached, so this message + // may still go nowhere on Windows — but silent failure is worse + // everywhere, and on Linux it reaches the file manager's stderr log. + let exe_path = match env::current_exe() { Ok(p) => p, - Err(_) => return ExitCode::from(1), + Err(e) => { + eprintln!("mdo-open: cannot locate its own executable: {e}"); + return ExitCode::from(1); + } }; - exe_path.pop(); // strip the file name, keep the directory - let args = env::args_os().skip(1).collect::>(); #[cfg(target_os = "windows")] @@ -77,7 +84,10 @@ fn main() -> ExitCode { if args.is_empty() { return match spawn_windows_onboarding(&exe_path) { Ok(()) => ExitCode::SUCCESS, - Err(_) => ExitCode::from(1), + Err(e) => { + eprintln!("mdo-open: {e}"); + ExitCode::from(1) + } }; } @@ -90,33 +100,47 @@ fn main() -> ExitCode { // instead of always reporting success. match spawn_windows_open(&exe_path, args) { Ok(status) => exit_code_from_status(status), - Err(_) => ExitCode::from(1), + Err(e) => { + eprintln!("mdo-open: {e}"); + ExitCode::from(1) + } } } #[cfg(not(target_os = "windows"))] { #[cfg(target_os = "linux")] - let mut cmd = if args.is_empty() { - let setup = exe_path.join(SETUP_BIN); - if !setup.exists() { - return ExitCode::from(1); + if args.is_empty() { + // No-file launch is onboarding, not open: hand off to the + // sibling `mdo-setup`. + let setup = match handler::sibling_binary(&exe_path, SETUP_BIN) { + Ok(path) => path, + Err(e) => { + eprintln!("mdo-open: {e}"); + return ExitCode::from(1); + } + }; + let mut cmd = Command::new(setup); + match cmd.status() { + Ok(status) => return exit_code_from_status(status), + Err(e) => { + eprintln!("mdo-open: {e}"); + return ExitCode::from(1); + } } - Command::new(setup) - } else { - let mut cmd = Command::new(exe_path.join(MDO_BIN)); - cmd.arg("--open"); - cmd.args(args); - cmd - }; + } - #[cfg(not(target_os = "linux"))] - let mut cmd = { - let mut cmd = Command::new(exe_path.join(MDO_BIN)); - cmd.arg("--open"); - cmd.args(args); - cmd + // Everything else is the forward: sibling mdo with the contract + // argv (`--open` plus the files as received). + let mdo_bin = match handler::sibling_binary(&exe_path, MDO_BIN) { + Ok(path) => path, + Err(e) => { + eprintln!("mdo-open: {e}"); + return ExitCode::from(1); + } }; + let mut cmd = Command::new(mdo_bin); + cmd.args(handler::open_argv(&args)); // Non-Windows binaries never had a console-flash problem to begin // with (that's Windows Explorer + console-subsystem-exe behavior), @@ -126,7 +150,10 @@ fn main() -> ExitCode { // in an already-attached terminal) and propagate its exit status. match cmd.status() { Ok(status) => exit_code_from_status(status), - Err(_) => ExitCode::from(1), + Err(e) => { + eprintln!("mdo-open: {e}"); + ExitCode::from(1) + } } } } @@ -137,10 +164,17 @@ fn main() -> ExitCode { /// a failure whose low byte is zero (e.g. 0x100) must not truncate to /// "success"; any failing status maps to a non-zero code. fn exit_code_from_status(status: std::process::ExitStatus) -> ExitCode { - if status.success() { + exit_code_from_parts(status.success(), status.code()) +} + +/// The truncation rule of [`exit_code_from_status`], split from +/// `ExitStatus` so it is unit-testable portably (Unix cannot even produce a +/// failed status with a zero low byte; Windows can). +fn exit_code_from_parts(success: bool, code: Option) -> ExitCode { + if success { return ExitCode::SUCCESS; } - match status.code() { + match code { Some(code) if code as u8 != 0 => ExitCode::from(code as u8), _ => ExitCode::FAILURE, } @@ -148,28 +182,18 @@ fn exit_code_from_status(status: std::process::ExitStatus) -> ExitCode { #[cfg(target_os = "windows")] fn spawn_windows_open( - exe_dir: &Path, + anchor_exe: &std::path::Path, args: Vec, ) -> io::Result { - let mut cmd = Command::new(exe_dir.join(MDO_BIN)); - cmd.arg("--open"); - cmd.args(args); + let mdo_bin = handler::sibling_binary(anchor_exe, MDO_BIN)?; + let mut cmd = Command::new(mdo_bin); + cmd.args(handler::open_argv(&args)); spawn_without_console(cmd) } #[cfg(target_os = "windows")] -fn spawn_windows_onboarding(exe_dir: &Path) -> io::Result<()> { - let mdo = exe_dir.join(MDO_BIN); - if !mdo.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - format!( - "expected {MDO_BIN} next to mdo-open.exe at {}", - mdo.display() - ), - )); - } - +fn spawn_windows_onboarding(anchor_exe: &std::path::Path) -> io::Result<()> { + let mdo = handler::sibling_binary(anchor_exe, MDO_BIN)?; mdo_cli::windows_setup::spawn_terminal_setup(&mdo) } @@ -189,3 +213,51 @@ fn spawn_without_console(mut cmd: Command) -> io::Result io::Result<()> { continue_after_launcher_registration(register_launcher()); - let mdo = sibling_binary("mdo")?; - if !mdo.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - format!("expected mdo next to mdo-setup at {}", mdo.display()), - )); - } + let mdo = mdo_cli::handler::sibling_binary(&std::env::current_exe()?, "mdo")?; // Already attached to a terminal (e.g. run from a shell): show guided setup // right here instead of spawning a second window. The `?` still @@ -254,12 +248,6 @@ mod linux_setup { .unwrap_or(false) } - fn sibling_binary(name: &str) -> io::Result { - let mut path = std::env::current_exe()?; - path.pop(); - Ok(path.join(name)) - } - #[cfg(test)] mod tests { use super::*; @@ -338,7 +326,7 @@ mod windows_setup { }; pub fn run() -> io::Result<()> { - let mdo = sibling_binary("mdo.exe")?; + let mdo = mdo_cli::handler::sibling_binary(&std::env::current_exe()?, "mdo.exe")?; if !mdo.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, @@ -375,12 +363,6 @@ mod windows_setup { Ok(button) } - fn sibling_binary(name: &str) -> io::Result { - let mut path = std::env::current_exe()?; - path.pop(); - Ok(path.join(name)) - } - fn wide(value: &str) -> Vec { value.encode_utf16().chain(iter::once(0)).collect() } diff --git a/src/file_manager.rs b/src/file_manager.rs index 3674b2f..8140994 100644 --- a/src/file_manager.rs +++ b/src/file_manager.rs @@ -4,6 +4,8 @@ use std::process::Command; #[cfg(target_os = "windows")] use std::process::Stdio; +use crate::handler; + const APP_DISPLAY_NAME: &str = "Open as HTML"; #[cfg(target_os = "linux")] const DESKTOP_FILE_NAME: &str = "mdo.desktop"; @@ -27,103 +29,284 @@ const SVG_ICON: &str = r##"