refactor: switch to clap with subcommands for CLI parsing
Replace hand-rolled arg parser with clap derive. Restructure as subcommands: import, cmd, script. Import subcommand supports --category, --measure, --time, --skip, --extract, --axis, --formula, --name, --no-wizard, and --output flags for configurable imports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
115
Cargo.lock
generated
115
Cargo.lock
generated
@ -23,6 +23,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
@ -107,6 +157,52 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
@ -394,6 +490,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"csv",
|
||||
"dirs",
|
||||
@ -442,6 +539,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@ -567,6 +670,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@ -1040,6 +1149,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
|
||||
@ -22,6 +22,7 @@ flate2 = "1"
|
||||
unicode-width = "0.2"
|
||||
dirs = "5"
|
||||
csv = "1"
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
|
||||
422
src/main.rs
422
src/main.rs
@ -12,43 +12,263 @@ use crate::import::csv_parser::csv_path_p;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use command::CommandResult;
|
||||
use draw::run_tui;
|
||||
use model::Model;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "improvise", about = "Multi-dimensional data modeling TUI")]
|
||||
struct Cli {
|
||||
/// Model file to open or create
|
||||
file: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Import JSON or CSV data, then open TUI (or save with --output)
|
||||
Import {
|
||||
/// Files to import (multiple CSVs merge with a "File" category)
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// Mark field as category dimension (repeatable)
|
||||
#[arg(long)]
|
||||
category: Vec<String>,
|
||||
|
||||
/// Mark field as numeric measure (repeatable)
|
||||
#[arg(long)]
|
||||
measure: Vec<String>,
|
||||
|
||||
/// Mark field as time/date category (repeatable)
|
||||
#[arg(long)]
|
||||
time: Vec<String>,
|
||||
|
||||
/// Skip/exclude a field from import (repeatable)
|
||||
#[arg(long)]
|
||||
skip: Vec<String>,
|
||||
|
||||
/// Extract date component, e.g. "Date:Month" (repeatable)
|
||||
#[arg(long)]
|
||||
extract: Vec<String>,
|
||||
|
||||
/// Set category axis, e.g. "Payee:row" (repeatable)
|
||||
#[arg(long)]
|
||||
axis: Vec<String>,
|
||||
|
||||
/// Add formula, e.g. "Profit = Revenue - Cost" (repeatable)
|
||||
#[arg(long)]
|
||||
formula: Vec<String>,
|
||||
|
||||
/// Model name (default: "Imported Model")
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
|
||||
/// Skip the interactive wizard
|
||||
#[arg(long)]
|
||||
no_wizard: bool,
|
||||
|
||||
/// Save to file instead of opening TUI
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Run a JSON command headless (repeatable)
|
||||
Cmd {
|
||||
/// JSON command strings
|
||||
json: Vec<String>,
|
||||
|
||||
/// Model file to load/save
|
||||
#[arg(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Run commands from a script file headless
|
||||
Script {
|
||||
/// Script file (one JSON command per line, # comments)
|
||||
path: PathBuf,
|
||||
|
||||
/// Model file to load/save
|
||||
#[arg(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let arg_config = parse_args(args);
|
||||
arg_config.run()
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
None => {
|
||||
let model = get_initial_model(&cli.file)?;
|
||||
run_tui(model, cli.file, None)
|
||||
}
|
||||
|
||||
trait Runnable {
|
||||
fn run(self: Box<Self>) -> Result<()>;
|
||||
}
|
||||
|
||||
struct CmdLineArgs {
|
||||
file_path: Option<PathBuf>,
|
||||
import_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Runnable for CmdLineArgs {
|
||||
fn run(self: Box<Self>) -> Result<()> {
|
||||
// Load or create model
|
||||
let model = get_initial_model(&self.file_path)?;
|
||||
|
||||
// Pre-TUI import: parse JSON or CSV and open wizard
|
||||
let import_value = if self.import_paths.is_empty() {
|
||||
None
|
||||
Some(Commands::Import {
|
||||
files,
|
||||
category,
|
||||
measure,
|
||||
time,
|
||||
skip,
|
||||
extract,
|
||||
axis,
|
||||
formula,
|
||||
name,
|
||||
no_wizard,
|
||||
output,
|
||||
}) => {
|
||||
let import_value = if files.is_empty() {
|
||||
anyhow::bail!("No files specified for import");
|
||||
} else {
|
||||
get_import_data(&self.import_paths)
|
||||
get_import_data(&files)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to parse import files"))?
|
||||
};
|
||||
|
||||
run_tui(model, self.file_path, import_value)
|
||||
let config = ImportConfig {
|
||||
categories: category,
|
||||
measures: measure,
|
||||
time_fields: time,
|
||||
skip_fields: skip,
|
||||
extractions: parse_colon_pairs(&extract),
|
||||
axes: parse_colon_pairs(&axis),
|
||||
formulas: formula,
|
||||
name,
|
||||
};
|
||||
|
||||
if no_wizard {
|
||||
run_headless_import(import_value, &config, output, cli.file)
|
||||
} else {
|
||||
run_wizard_import(import_value, &config, cli.file)
|
||||
}
|
||||
}
|
||||
|
||||
Some(Commands::Cmd { json, file }) => run_headless_commands(&json, &file),
|
||||
|
||||
Some(Commands::Script { path, file }) => run_headless_script(&path, &file),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Import config ────────────────────────────────────────────────────────────
|
||||
|
||||
struct ImportConfig {
|
||||
categories: Vec<String>,
|
||||
measures: Vec<String>,
|
||||
time_fields: Vec<String>,
|
||||
skip_fields: Vec<String>,
|
||||
extractions: Vec<(String, String)>,
|
||||
axes: Vec<(String, String)>,
|
||||
formulas: Vec<String>,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_colon_pairs(args: &[String]) -> Vec<(String, String)> {
|
||||
args.iter()
|
||||
.filter_map(|s| {
|
||||
let (a, b) = s.split_once(':')?;
|
||||
Some((a.to_string(), b.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply_config_to_pipeline(
|
||||
pipeline: &mut import::wizard::ImportPipeline,
|
||||
config: &ImportConfig,
|
||||
) {
|
||||
use import::analyzer::{DateComponent, FieldKind};
|
||||
|
||||
// Override field kinds
|
||||
for p in &mut pipeline.proposals {
|
||||
if config.categories.contains(&p.field) {
|
||||
p.kind = FieldKind::Category;
|
||||
p.accepted = true;
|
||||
} else if config.measures.contains(&p.field) {
|
||||
p.kind = FieldKind::Measure;
|
||||
p.accepted = true;
|
||||
} else if config.time_fields.contains(&p.field) {
|
||||
p.kind = FieldKind::TimeCategory;
|
||||
p.accepted = true;
|
||||
} else if config.skip_fields.contains(&p.field) {
|
||||
p.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply date component extractions
|
||||
for (field, comp_str) in &config.extractions {
|
||||
let component = match comp_str.to_lowercase().as_str() {
|
||||
"year" => DateComponent::Year,
|
||||
"month" => DateComponent::Month,
|
||||
"quarter" => DateComponent::Quarter,
|
||||
_ => continue,
|
||||
};
|
||||
for p in &mut pipeline.proposals {
|
||||
if p.field == *field && !p.date_components.contains(&component) {
|
||||
p.date_components.push(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set formulas
|
||||
pipeline.formulas = config.formulas.clone();
|
||||
|
||||
// Set model name
|
||||
if let Some(ref name) = config.name {
|
||||
pipeline.model_name = name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_axis_overrides(model: &mut Model, axes: &[(String, String)]) {
|
||||
use view::Axis;
|
||||
let view = model.active_view_mut();
|
||||
for (cat, axis_str) in axes {
|
||||
let axis = match axis_str.to_lowercase().as_str() {
|
||||
"row" => Axis::Row,
|
||||
"column" | "col" => Axis::Column,
|
||||
"page" => Axis::Page,
|
||||
"none" => Axis::None,
|
||||
_ => continue,
|
||||
};
|
||||
view.set_axis(cat, axis);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_headless_import(
|
||||
import_value: Value,
|
||||
config: &ImportConfig,
|
||||
output: Option<PathBuf>,
|
||||
model_file: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
let mut pipeline = import::wizard::ImportPipeline::new(import_value);
|
||||
apply_config_to_pipeline(&mut pipeline, config);
|
||||
let mut model = pipeline.build_model()?;
|
||||
model.normalize_view_state();
|
||||
apply_axis_overrides(&mut model, &config.axes);
|
||||
|
||||
if let Some(path) = output.or(model_file) {
|
||||
persistence::save(&model, &path)?;
|
||||
eprintln!("Saved to {}", path.display());
|
||||
} else {
|
||||
eprintln!("No output path specified; use -o <path> or provide a model file");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_wizard_import(
|
||||
import_value: Value,
|
||||
_config: &ImportConfig,
|
||||
model_file: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
let model = get_initial_model(&model_file)?;
|
||||
// Pre-configure will happen inside the TUI via the wizard
|
||||
// For now, pass import_value and let the wizard handle it
|
||||
// TODO: pass config to wizard for pre-population
|
||||
run_tui(model, model_file, Some(import_value))
|
||||
}
|
||||
|
||||
// ── Import data loading ──────────────────────────────────────────────────────
|
||||
|
||||
fn get_import_data(paths: &[PathBuf]) -> Option<Value> {
|
||||
let all_csv = paths.iter().all(|p| csv_path_p(p));
|
||||
|
||||
@ -94,34 +314,13 @@ fn get_import_data(paths: &[PathBuf]) -> Option<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
enum HeadlessMode {
|
||||
Commands(Vec<String>),
|
||||
Script(Option<PathBuf>),
|
||||
}
|
||||
struct HeadlessArgs {
|
||||
file_path: Option<PathBuf>,
|
||||
mode: HeadlessMode,
|
||||
}
|
||||
// ── Headless command execution ───────────────────────────────────────────────
|
||||
|
||||
impl Runnable for HeadlessArgs {
|
||||
fn run(self: Box<Self>) -> Result<()> {
|
||||
let mut model = get_initial_model(&self.file_path)?;
|
||||
fn run_headless_commands(cmds: &[String], file: &Option<PathBuf>) -> Result<()> {
|
||||
let mut model = get_initial_model(file)?;
|
||||
let mut exit_code = 0;
|
||||
match self.mode {
|
||||
HeadlessMode::Script(script) => {
|
||||
if let Some(script_path) = script {
|
||||
let content = std::fs::read_to_string(&script_path)?;
|
||||
let mut cmds: Vec<String> = Vec::new();
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty()
|
||||
&& !trimmed.starts_with("//")
|
||||
&& !trimmed.starts_with('#')
|
||||
{
|
||||
cmds.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
for raw_cmd in &cmds {
|
||||
|
||||
for raw_cmd in cmds {
|
||||
let parsed: command::Command = match serde_json::from_str(raw_cmd) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@ -137,126 +336,27 @@ impl Runnable for HeadlessArgs {
|
||||
}
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
}
|
||||
};
|
||||
}
|
||||
HeadlessMode::Commands(cmds) => {
|
||||
for raw_cmd in &cmds {
|
||||
let parsed: command::Command = match serde_json::from_str(raw_cmd) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let r = CommandResult::err(format!("JSON parse error: {e}"));
|
||||
println!("{}", serde_json::to_string(&r)?);
|
||||
exit_code = 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let result = command::dispatch(&mut model, &parsed);
|
||||
if !result.ok {
|
||||
exit_code = 1;
|
||||
}
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = self.file_path {
|
||||
persistence::save(&model, &path)?;
|
||||
if let Some(path) = file {
|
||||
persistence::save(&model, path)?;
|
||||
}
|
||||
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
fn run_headless_script(script_path: &PathBuf, file: &Option<PathBuf>) -> Result<()> {
|
||||
let content = std::fs::read_to_string(script_path)?;
|
||||
let cmds: Vec<String> = content
|
||||
.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty() && !l.starts_with("//") && !l.starts_with('#'))
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
run_headless_commands(&cmds, file)
|
||||
}
|
||||
|
||||
struct HelpArgs;
|
||||
|
||||
impl Runnable for HelpArgs {
|
||||
fn run(self: Box<Self>) -> Result<()> {
|
||||
println!("improvise — multi-dimensional data modeling TUI\n");
|
||||
println!("USAGE:");
|
||||
println!(" improvise [file.improv] Open or create a model");
|
||||
println!(" improvise --import data.json Import JSON or CSV then open TUI");
|
||||
println!(
|
||||
" improvise --import a.csv b.csv Import multiple CSVs (filenames become a category)"
|
||||
);
|
||||
println!(" improvise --cmd '{{...}}' Run JSON command(s) headless (repeatable)");
|
||||
println!(" improvise --script cmds.jsonl Run commands from file headless (exclusive with --cmd)");
|
||||
println!("\nTUI KEYS (vim-style):");
|
||||
println!(" : Command mode (:q :w :import :add-cat :formula …)");
|
||||
println!(" hjkl / ↑↓←→ Navigate grid");
|
||||
println!(" i / Enter Edit cell (Insert mode)");
|
||||
println!(" Esc Return to Normal mode");
|
||||
println!(" x Clear cell");
|
||||
println!(" yy / p Yank / paste cell value");
|
||||
println!(" gg / G First / last row");
|
||||
println!(" 0 / $ First / last column");
|
||||
println!(" Ctrl+D/U Scroll half-page down / up");
|
||||
println!(" / n N Search / next / prev");
|
||||
println!(" [ ] Cycle page-axis filter");
|
||||
println!(" T Tile-select (pivot) mode");
|
||||
println!(" F C V Toggle Formulas / Categories / Views panel");
|
||||
println!(" ZZ Save and quit");
|
||||
println!(" ? Help");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args(args: Vec<String>) -> Box<dyn Runnable> {
|
||||
let mut file_path: Option<PathBuf> = None;
|
||||
let mut headless_cmds: Vec<String> = Vec::new();
|
||||
let mut headless_script: Option<PathBuf> = None;
|
||||
let mut import_paths: Vec<PathBuf> = Vec::new();
|
||||
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--cmd" | "-c" => {
|
||||
i += 1;
|
||||
if let Some(cmd) = args.get(i).cloned() {
|
||||
headless_cmds.push(cmd);
|
||||
}
|
||||
}
|
||||
"--script" | "-s" => {
|
||||
i += 1;
|
||||
headless_script = args.get(i).map(PathBuf::from);
|
||||
}
|
||||
"--import" => {
|
||||
i += 1;
|
||||
while i < args.len() && !args[i].starts_with('-') {
|
||||
import_paths.push(PathBuf::from(&args[i]));
|
||||
i += 1;
|
||||
}
|
||||
continue; // skip the i += 1 at the bottom
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
return Box::new(HelpArgs);
|
||||
}
|
||||
arg if !arg.starts_with('-') => {
|
||||
file_path = Some(PathBuf::from(arg));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if !headless_cmds.is_empty() && headless_script.is_some() {
|
||||
eprintln!("Error: --cmd and --script cannot be used together");
|
||||
std::process::exit(1);
|
||||
} else if !headless_cmds.is_empty() || headless_script.is_some() {
|
||||
Box::new(HeadlessArgs {
|
||||
file_path,
|
||||
mode: if headless_script.is_some() {
|
||||
HeadlessMode::Script(headless_script)
|
||||
} else {
|
||||
HeadlessMode::Commands(headless_cmds)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
Box::new(CmdLineArgs {
|
||||
file_path,
|
||||
import_paths,
|
||||
})
|
||||
}
|
||||
}
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_initial_model(file_path: &Option<PathBuf>) -> Result<Model> {
|
||||
if let Some(ref path) = file_path {
|
||||
|
||||
Reference in New Issue
Block a user