cad-editor/src/cli.rs

110 lines
4 KiB
Rust
Raw Normal View History

//! Command-line interface.
//!
//! Parsing lives here; `main` interprets the result. Three run modes split out
//! of the parsed args:
//! - `--serve` headless JSON automation server (see `app::serve`)
//! - `--export IN OUT` one-shot headless format conversion, then exit
//! - otherwise launch the GUI editor, configured via [`GuiConfig`]
//!
//! GUI-only options (open-file, `--new`, `--read-only`, `--script`) are stashed
//! in [`GUI_CONFIG`] for `app::boot` to read, since the iced daemon boots with
//! no arguments of its own.
use std::path::PathBuf;
use std::sync::OnceLock;
use clap::Parser;
/// Open CAD Studio command-line options.
#[derive(Parser, Debug, Default)]
#[command(
name = "OpenCADStudio",
version,
about = "Open CAD Studio — 2D/3D CAD editor",
long_about = None,
)]
pub struct Cli {
feat(open): open drawings in the running editor Double-clicking a drawing started a whole second copy of the app. Now it lands as a tab in the editor already open, and that window comes forward. The election is the bind itself: every GUI launch tries a deterministic per-user loopback port, and the OS grants it to exactly one process and releases it on any death, SIGKILL included. Nothing to reap, no PID to probe, no stale lock after a crash. The winner serves the port from an iced subscription; the loser checks it is talking to a matching editor, hands the paths over and exits. Every surprise — no answer, a stranger on the port, a timeout — boots a normal window, i.e. the old behaviour: the feature can degrade, but it cannot lose the file. The gate in main is positional, after every headless mode has returned. The plugin runner is this same binary re-spawning itself, so a flag list would rot the first time a mode is added; a position cannot. Handing a drawing over that is already open switches to its tab instead of loading a second copy, comparing resolved paths so a symlink or a `..` hop is still recognised as the same file. `opening` is a single slot that a second open would overwrite, and on_file_opened drops any result arriving once it is clear — so drawings handed over at once would silently vanish. They queue in pending_opens, drained from every path that clears the slot, error and cancel included. Exec becomes %F so one launch takes the whole selection; the CLI takes a list and boot fans it out through the same door. --new-instance forces a separate process; --read-only / --script / --new always get their own. Not reused: the automation server. It dispatches arbitrary commands and writes arbitrary paths, and its open replaces the active tab's document in place — publishing that on a port every local process can reach would be unforced privilege escalation. Only its JSON-lines framing is shared. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:18:16 +03:00
/// CAD files to open at startup (.dwg / .dxf). Also how the OS file
/// association launches us when drawings are double-clicked — selecting
/// several hands them all to one launch, so this takes a list.
pub files: Vec<PathBuf>,
/// Start with a new empty drawing, ignoring any file argument.
#[arg(long)]
pub new: bool,
feat(open): open drawings in the running editor Double-clicking a drawing started a whole second copy of the app. Now it lands as a tab in the editor already open, and that window comes forward. The election is the bind itself: every GUI launch tries a deterministic per-user loopback port, and the OS grants it to exactly one process and releases it on any death, SIGKILL included. Nothing to reap, no PID to probe, no stale lock after a crash. The winner serves the port from an iced subscription; the loser checks it is talking to a matching editor, hands the paths over and exits. Every surprise — no answer, a stranger on the port, a timeout — boots a normal window, i.e. the old behaviour: the feature can degrade, but it cannot lose the file. The gate in main is positional, after every headless mode has returned. The plugin runner is this same binary re-spawning itself, so a flag list would rot the first time a mode is added; a position cannot. Handing a drawing over that is already open switches to its tab instead of loading a second copy, comparing resolved paths so a symlink or a `..` hop is still recognised as the same file. `opening` is a single slot that a second open would overwrite, and on_file_opened drops any result arriving once it is clear — so drawings handed over at once would silently vanish. They queue in pending_opens, drained from every path that clears the slot, error and cancel included. Exec becomes %F so one launch takes the whole selection; the CLI takes a list and boot fans it out through the same door. --new-instance forces a separate process; --read-only / --script / --new always get their own. Not reused: the automation server. It dispatches arbitrary commands and writes arbitrary paths, and its open replaces the active tab's document in place — publishing that on a port every local process can reach would be unforced privilege escalation. Only its JSON-lines framing is shared. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:18:16 +03:00
/// Always start a new editor process, even when one is already running.
/// Without this, opening a drawing hands it to the running editor as a tab.
#[arg(long)]
pub new_instance: bool,
/// Open read-only: editing is allowed but saving is disabled.
#[arg(long)]
pub read_only: bool,
/// Restrict the GPU backend (e.g. dx12, vulkan, gl, metal). Sets WGPU_BACKEND.
#[arg(long, value_name = "BACKEND")]
pub backend: Option<String>,
/// Safe mode: force the GL backend, for flaky/hybrid GPU drivers.
#[arg(long, visible_alias = "no-gpu")]
pub safe_mode: bool,
/// Force the packed renderer path that avoids shader storage buffers.
/// Normally selected automatically for adapters with insufficient limits.
#[arg(long)]
pub compat_renderer: bool,
/// Run the headless JSON automation server (stdin/stdout, or --port).
#[arg(long)]
pub serve: bool,
/// TCP port for --serve (defaults to stdin/stdout).
#[arg(long, value_name = "PORT")]
pub port: Option<u16>,
/// Headless convert: read IN, write OUT (format from OUT's extension), exit.
#[arg(long, num_args = 2, value_names = ["IN", "OUT"])]
pub export: Option<Vec<PathBuf>>,
/// Run a command script at startup: one command line per line of FILE.
#[arg(long, value_name = "FILE")]
pub script: Option<PathBuf>,
/// Log level (error|warn|info|debug|trace). Also honours RUST_LOG.
#[arg(long, value_name = "LEVEL")]
pub log: Option<String>,
2026-06-23 22:54:33 +02:00
/// Internal: run as the plugin runner child process.
#[arg(long, value_names = ["SOCKET", "CDYLIB"], num_args = 2, hide = true)]
pub ocs_plugin_runner: Option<Vec<String>>,
feat(thumbnails): embed DWG previews and wire cross-platform file thumbnails Round-trip a raster preview through the DWG's embedded preview slot and surface it to the OS file managers, so drawings show their contents in Explorer / Finder / Nautilus instead of a generic document icon. OCS: * io::thumbnail — rasterize the scene to a BMP DIB on save (embedded via acadrust's new Preview type) and read it back on open for the Start page recent list; `--dwg-thumbnail IN OUT SIZE` CLI extracts a badged PNG for external thumbnailers. * io::file_association::install_thumbnailer — self-install the OS integration on startup: Linux writes a .thumbnailer + hicolor mimetype icons; Windows registers the IThumbnailProvider DLL under HKCU. Shared core: * crates/dwg-thumbnailer — lean (image-only) preview extractor + the `badge_dwg` full-width "DWG" band, used by every platform so the ribbon is single-sourced. Ships an rlib+staticlib. * crates/dwg-thumbnailer-win — IThumbnailProvider COM in-proc server (cfg(windows), CI-built as a workspace member). * macos/ — QuickLook thumbnail extension (Swift + C-ABI bridge to the core staticlib), assembled into a .appex. Icons & packaging (single SVG source -> per-platform assets in CI): * assets/mimetypes/image-vnd.{dwg,dxf}.svg — themed file icons. * packaging: WiX ships dwg/dxf.ico + the thumbnail DLL and points the ProgIds at them; Info.plist gets CFBundleTypeIconFile + the QuickLook extension in Contents/PlugIns. * release.yml — generate .ico (Windows) and .icns (macOS) from the SVGs, build the QuickLook .appex, and bundle everything. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 01:27:17 +03:00
/// Internal: write a DWG's embedded preview to a PNG for the OS file-manager
/// thumbnailer (`<IN> <OUT> <SIZE>`). Handled before the GUI starts.
#[arg(long, value_names = ["IN", "OUT", "SIZE"], num_args = 3, hide = true)]
pub dwg_thumbnail: Option<Vec<String>>,
}
/// GUI startup configuration, handed from `main` to `app::boot` out-of-band
/// because the iced daemon's boot closure takes no arguments.
#[derive(Debug, Default, Clone)]
pub struct GuiConfig {
feat(open): open drawings in the running editor Double-clicking a drawing started a whole second copy of the app. Now it lands as a tab in the editor already open, and that window comes forward. The election is the bind itself: every GUI launch tries a deterministic per-user loopback port, and the OS grants it to exactly one process and releases it on any death, SIGKILL included. Nothing to reap, no PID to probe, no stale lock after a crash. The winner serves the port from an iced subscription; the loser checks it is talking to a matching editor, hands the paths over and exits. Every surprise — no answer, a stranger on the port, a timeout — boots a normal window, i.e. the old behaviour: the feature can degrade, but it cannot lose the file. The gate in main is positional, after every headless mode has returned. The plugin runner is this same binary re-spawning itself, so a flag list would rot the first time a mode is added; a position cannot. Handing a drawing over that is already open switches to its tab instead of loading a second copy, comparing resolved paths so a symlink or a `..` hop is still recognised as the same file. `opening` is a single slot that a second open would overwrite, and on_file_opened drops any result arriving once it is clear — so drawings handed over at once would silently vanish. They queue in pending_opens, drained from every path that clears the slot, error and cancel included. Exec becomes %F so one launch takes the whole selection; the CLI takes a list and boot fans it out through the same door. --new-instance forces a separate process; --read-only / --script / --new always get their own. Not reused: the automation server. It dispatches arbitrary commands and writes arbitrary paths, and its open replaces the active tab's document in place — publishing that on a port every local process can reach would be unforced privilege escalation. Only its JSON-lines framing is shared. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:18:16 +03:00
/// Files to open on launch (empty for a blank session).
pub files: Vec<PathBuf>,
/// Open a fresh drawing tab on launch instead of the welcome screen.
pub new: bool,
/// Saving disabled for this session.
pub read_only: bool,
/// Force storage-buffer-free wire and hatch pipelines.
pub compat_renderer: bool,
/// Command lines to run once the editor is up.
pub script_lines: Vec<String>,
}
/// Set once by `main` before the GUI boots; read by `app::boot`.
pub static GUI_CONFIG: OnceLock<GuiConfig> = OnceLock::new();
/// The GUI config, or a default empty one if `main` never set it (e.g. tests).
pub fn gui_config() -> GuiConfig {
GUI_CONFIG.get().cloned().unwrap_or_default()
}