feat: add clap CLI arg parser, replacing MANDEL_* debug env vars

Prep work for a headless render mode: native debug flags (kind, power,
julia, share, view, de, buddhabrot, export, ...) are now parsed with
clap instead of read from env vars, with proper --help/--version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GFBP9qrNg6NGJWzzP36xe
This commit is contained in:
2026-09-19 17:51:03 +02:00
co-authored by Claude Sonnet 5
parent 3797a33230
commit ff5ac0f262
4 changed files with 114 additions and 27 deletions
+1
View File
@@ -13,6 +13,7 @@ png = "0.18.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
env_logger = "0.11.11" env_logger = "0.11.11"
clap = { version = "4.5.51", features = ["derive"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
futures-channel = { version = "0.3.34", default-features = false, features = ["alloc", "std"] } futures-channel = { version = "0.3.34", default-features = false, features = ["alloc", "std"] }
+26 -26
View File
@@ -4,6 +4,8 @@ use eframe::CreationContext;
use eframe::egui_wgpu; use eframe::egui_wgpu;
use eframe::egui_wgpu::wgpu; use eframe::egui_wgpu::wgpu;
#[cfg(not(target_arch = "wasm32"))]
use crate::cli::Cli;
use crate::fractal::{ use crate::fractal::{
BuddhabrotCallback, BuddhabrotRenderer, BuddhabrotUniforms, ExportRender, FractalCallback, BuddhabrotCallback, BuddhabrotRenderer, BuddhabrotUniforms, ExportRender, FractalCallback,
FractalKind, FractalRenderer, MAX_REF_POINTS, ShareState, Uniforms, FractalKind, FractalRenderer, MAX_REF_POINTS, ShareState, Uniforms,
@@ -15,6 +17,8 @@ use crate::view::{
Big, DEFAULT_HALF_HEIGHT, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str, Big, DEFAULT_HALF_HEIGHT, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str,
precision_for, precision_for,
}; };
#[cfg(not(target_arch = "wasm32"))]
use clap::Parser;
const BAILOUT_SQ: f32 = 1.0e6; const BAILOUT_SQ: f32 = 1.0e6;
/// Cap on exported image dimension (px), to stay within GPU texture limits. /// Cap on exported image dimension (px), to stay within GPU texture limits.
@@ -338,6 +342,9 @@ pub struct FractalApp {
export_requested: bool, export_requested: bool,
/// Progress/handle for an in-flight PNG export, if any. /// Progress/handle for an in-flight PNG export, if any.
export: Option<Arc<Mutex<ExportShared>>>, export: Option<Arc<Mutex<ExportShared>>>,
/// Output path for `--export` (native CLI only); falls back to a
/// timestamped name when unset.
export_path: Option<String>,
/// Short status line (saved path, "link copied", errors). /// Short status line (saved path, "link copied", errors).
status: Option<String>, status: Option<String>,
@@ -442,6 +449,7 @@ impl FractalApp {
last_interact_time: -1.0e9, last_interact_time: -1.0e9,
export_requested: false, export_requested: false,
export: None, export: None,
export_path: None,
status: None, status: None,
center_re_edit, center_re_edit,
center_im_edit, center_im_edit,
@@ -457,28 +465,19 @@ impl FractalApp {
} }
} }
// Debug/testing hooks. // Debug/testing hooks, driven by CLI flags.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
if let Ok(k) = std::env::var("MANDEL_KIND") { let cli = Cli::parse();
app.kind = match k.trim().to_ascii_lowercase().as_str() {
"burningship" | "burning_ship" | "ship" => FractalKind::BurningShip, if let Some(k) = cli.kind {
"tricorn" | "mandelbar" => FractalKind::Tricorn, app.kind = k.into();
"multibrot" | "multi" => FractalKind::Multibrot, if let Some(p) = cli.power {
"celtic" => FractalKind::Celtic,
"perpendicular" | "perp" => FractalKind::Perpendicular,
"buffalo" => FractalKind::Buffalo,
"phoenix" => FractalKind::Phoenix,
_ => FractalKind::Mandelbrot,
};
if let Ok(p) = std::env::var("MANDEL_POWER")
&& let Ok(p) = p.trim().parse::<u32>()
{
app.power = p.clamp(2, 8); app.power = p.clamp(2, 8);
} }
app.view = Self::default_view_for(app.mode, app.kind); app.view = Self::default_view_for(app.mode, app.kind);
} }
if let Ok(jc) = std::env::var("MANDEL_JULIA") { if let Some(jc) = cli.julia {
let p: Vec<&str> = jc.split(',').collect(); let p: Vec<&str> = jc.split(',').collect();
if let (Some(Ok(re)), Some(Ok(im))) = ( if let (Some(Ok(re)), Some(Ok(im))) = (
p.first().map(|s| s.trim().parse::<f64>()), p.first().map(|s| s.trim().parse::<f64>()),
@@ -489,26 +488,25 @@ impl FractalApp {
app.view = Self::default_view_for(FractalMode::Julia, app.kind); app.view = Self::default_view_for(FractalMode::Julia, app.kind);
} }
} }
if let Ok(frag) = std::env::var("MANDEL_SHARE") if let Some(frag) = cli.share
&& let Some(state) = ShareState::decode(&frag) && let Some(state) = ShareState::decode(&frag)
{ {
app.apply_share(&state); app.apply_share(&state);
} }
if let Ok(spec) = std::env::var("MANDEL_VIEW") { if let Some(spec) = cli.view {
app.apply_view_spec(&spec); app.apply_view_spec(&spec);
} }
if std::env::var("MANDEL_DE").is_ok() { if cli.de {
app.de_coloring = true; app.de_coloring = true;
} }
if std::env::var("MANDEL_BUDDHABROT").is_ok() { if cli.buddhabrot {
app.buddhabrot = true; app.buddhabrot = true;
} }
if let Ok(p) = std::env::var("MANDEL_BUDDHA_PALETTE") if let Some(p) = cli.buddha_palette {
&& let Ok(p) = p.trim().parse::<u32>()
{
app.buddha_palette = p.min(BUDDHA_PALETTE_NAMES.len() as u32 - 1); app.buddha_palette = p.min(BUDDHA_PALETTE_NAMES.len() as u32 - 1);
} }
if std::env::var("MANDEL_EXPORT").is_ok() { app.export_path = cli.export_path;
if cli.export {
app.export_requested = true; app.export_requested = true;
} }
} }
@@ -912,8 +910,10 @@ impl FractalApp {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let name = std::env::var("MANDEL_EXPORT_PATH") let name = self
.unwrap_or_else(|_| format!("fractal-{}.png", unix_timestamp())); .export_path
.clone()
.unwrap_or_else(|| format!("fractal-{}.png", unix_timestamp()));
std::thread::spawn(move || { std::thread::spawn(move || {
let er = ExportRender::new( let er = ExportRender::new(
&device, &device,
+84
View File
@@ -0,0 +1,84 @@
// Native command-line arguments. Currently mirrors the old `MANDEL_*` debug
// env vars one-for-one; this is the foundation a future headless (no-window,
// render-to-file) mode will build on.
use clap::{Parser, ValueEnum};
use crate::fractal::FractalKind;
#[derive(Parser, Debug, Default)]
#[command(name = "mandelbrot", about = "Deep-zoom fractal explorer", version)]
pub struct Cli {
/// Fractal formula to render.
#[arg(long, value_enum)]
pub kind: Option<KindArg>,
/// Exponent for the Multibrot kind (z -> z^power + c), clamped to [2, 8].
#[arg(long)]
pub power: Option<u32>,
/// Start in Julia mode with this seed constant.
#[arg(long, value_name = "RE,IM")]
pub julia: Option<String>,
/// Restore a view from a share-link fragment (the part after '#').
#[arg(long, value_name = "FRAGMENT")]
pub share: Option<String>,
/// Jump to a view on startup.
#[arg(long, value_name = "RE,IM,HALF_HEIGHT[,ITERATIONS]")]
pub view: Option<String>,
/// Enable distance-estimation shading.
#[arg(long)]
pub de: bool,
/// Switch to the Buddhabrot renderer.
#[arg(long)]
pub buddhabrot: bool,
/// Buddhabrot tonemap palette index.
#[arg(long, value_name = "INDEX")]
pub buddha_palette: Option<u32>,
/// Render a PNG export on startup.
#[arg(long)]
pub export: bool,
/// Output path for --export (default: fractal-<timestamp>.png).
#[arg(long, value_name = "PATH")]
pub export_path: Option<String>,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum KindArg {
Mandelbrot,
#[value(alias = "ship")]
BurningShip,
#[value(alias = "mandelbar")]
Tricorn,
#[value(alias = "multi")]
Multibrot,
Celtic,
#[value(alias = "perp")]
Perpendicular,
Buffalo,
Phoenix,
Lambda,
}
impl From<KindArg> for FractalKind {
fn from(k: KindArg) -> Self {
match k {
KindArg::Mandelbrot => FractalKind::Mandelbrot,
KindArg::BurningShip => FractalKind::BurningShip,
KindArg::Tricorn => FractalKind::Tricorn,
KindArg::Multibrot => FractalKind::Multibrot,
KindArg::Celtic => FractalKind::Celtic,
KindArg::Perpendicular => FractalKind::Perpendicular,
KindArg::Buffalo => FractalKind::Buffalo,
KindArg::Phoenix => FractalKind::Phoenix,
KindArg::Lambda => FractalKind::Lambda,
}
}
}
+3 -1
View File
@@ -9,10 +9,12 @@
// and calls the wasm `main`, which boots eframe onto the page's <canvas>. // and calls the wasm `main`, which boots eframe onto the page's <canvas>.
mod app; mod app;
mod lights;
mod fractal; mod fractal;
mod lights;
mod view; mod view;
#[cfg(not(target_arch = "wasm32"))]
mod cli;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod worker; mod worker;