Skip to main content

monsoon_cli/cli/
args.rs

1//! CLI argument definitions using clap derive macros.
2//!
3//! This module defines all command-line arguments for the NES emulator CLI.
4//! Arguments are organized into logical groups matching the documentation.
5
6use std::path::PathBuf;
7
8use clap::{Args, Parser, ValueEnum, value_parser};
9use serde::Deserialize;
10
11/// NES Emulator CLI - A cycle-accurate NES emulator with comprehensive CLI support
12#[derive(Parser, Debug, Clone, Default)]
13#[command(name = "nes_main")]
14#[command(version, about, long_about = None)]
15#[command(after_help = "For more information, see docs/CLI_INTERFACE.md")]
16pub struct CliArgs {
17    /// Suppress non-error output
18    #[arg(short, long, default_value_t = false)]
19    pub quiet: bool,
20
21    /// Enable verbose output
22    #[arg(short, long, default_value_t = false)]
23    pub verbose: bool,
24
25    /// Load configuration from TOML file
26    #[arg(short, long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
27    pub config: Option<PathBuf>,
28
29    #[command(flatten)]
30    pub rom: RomArgs,
31
32    #[command(flatten)]
33    pub savestate: SavestateArgs,
34
35    #[command(flatten)]
36    pub memory: MemoryArgs,
37
38    #[command(flatten)]
39    pub power: PowerArgs,
40
41    #[command(flatten)]
42    pub palette: PaletteArgs,
43
44    #[command(flatten)]
45    pub video: VideoArgs,
46
47    #[command(flatten)]
48    pub execution: ExecutionArgs,
49
50    #[command(flatten)]
51    pub output: OutputArgs,
52}
53
54/// ROM loading arguments
55#[derive(Args, Debug, Clone, Default)]
56pub struct RomArgs {
57    /// Path to ROM file
58    #[arg(short, long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
59    pub rom: Option<PathBuf>,
60
61    /// Print ROM information and exit
62    #[arg(long, default_value_t = false)]
63    pub rom_info: bool,
64}
65
66/// Savestate operation arguments
67#[derive(Args, Debug, Clone, Default)]
68pub struct SavestateArgs {
69    /// Load savestate from file
70    #[arg(short = 'l', long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
71    pub load_state: Option<PathBuf>,
72
73    /// Save state to file on exit
74    #[arg(short = 's', long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
75    pub save_state: Option<PathBuf>,
76
77    /// Read savestate from stdin
78    #[arg(long, default_value_t = false)]
79    pub state_stdin: bool,
80
81    /// Write savestate to stdout on exit
82    #[arg(long, default_value_t = false)]
83    pub state_stdout: bool,
84
85    /// When to save state (exit, stop, cycle:N, pc:ADDR, frame:N)
86    #[arg(long)]
87    pub save_state_on: Option<String>,
88
89    /// Savestate format for saving (binary or json)
90    #[arg(long, default_value = "binary")]
91    pub state_format: SavestateFormat,
92}
93
94/// Savestate format options
95#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Deserialize)]
96pub enum SavestateFormat {
97    /// Binary format (smaller, faster, default)
98    #[default]
99    Binary,
100    /// JSON format (human-readable, editable)
101    Json,
102}
103
104/// Memory operation arguments
105#[derive(Args, Debug, Clone, Default)]
106pub struct MemoryArgs {
107    /// Read CPU memory range (e.g., 0x0000-0x07FF or 0x6000:0x100)
108    #[arg(long)]
109    pub read_cpu: Option<String>,
110
111    /// Read PPU memory range (e.g., 0x0000-0x1FFF)
112    #[arg(long)]
113    pub read_ppu: Option<String>,
114
115    /// Dump OAM (sprite) memory
116    #[arg(long, default_value_t = false)]
117    pub dump_oam: bool,
118
119    /// Dump nametables
120    #[arg(long, default_value_t = false)]
121    pub dump_nametables: bool,
122
123    /// Dump palette RAM (32 bytes at $3F00-$3F1F)
124    #[arg(long, default_value_t = false)]
125    pub dump_palette: bool,
126
127    /// Initialize CPU memory (ADDR=VALUE or ADDR=V1,V2,...)
128    #[arg(long, action = clap::ArgAction::Append)]
129    pub init_cpu: Vec<String>,
130
131    /// Initialize PPU memory (ADDR=VALUE or ADDR=V1,V2,...)
132    #[arg(long, action = clap::ArgAction::Append)]
133    pub init_ppu: Vec<String>,
134
135    /// Initialize OAM memory (ADDR=VALUE or ADDR=V1,V2,...)
136    #[arg(long, action = clap::ArgAction::Append)]
137    pub init_oam: Vec<String>,
138
139    /// Load init values from file (JSON/TOML/binary)
140    #[arg(long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
141    pub init_file: Option<PathBuf>,
142}
143
144/// Power control arguments
145#[derive(Args, Debug, Clone, Default)]
146pub struct PowerArgs {
147    /// Don't auto-power on after ROM load
148    #[arg(long, default_value_t = false)]
149    pub no_power: bool,
150
151    /// Reset after loading
152    #[arg(long, default_value_t = false)]
153    pub reset: bool,
154}
155
156/// Palette configuration arguments
157#[derive(Args, Debug, Clone, Default)]
158pub struct PaletteArgs {
159    /// Path to .pal RGB palette file
160    #[arg(short, long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
161    pub palette: Option<PathBuf>,
162
163    /// Use built-in palette by name (2C02G, composite)
164    #[arg(long)]
165    pub palette_builtin: Option<BuiltinPalette>,
166}
167
168/// Built-in palette options
169#[derive(Debug, Clone, Copy, ValueEnum, Default)]
170pub enum BuiltinPalette {
171    /// Standard 2C02G palette (default)
172    #[default]
173    #[value(name = "2C02G")]
174    Nes2C02G,
175    /// NTSC composite simulation
176    Composite,
177}
178
179/// Video/screenshot export arguments
180#[derive(Args, Debug, Clone, Default)]
181pub struct VideoArgs {
182    /// Save screenshot on exit
183    #[arg(long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
184    pub screenshot: Option<PathBuf>,
185
186    /// When to capture screenshot (exit, stop, cycle:N, pc:ADDR, frame:N)
187    #[arg(long)]
188    pub screenshot_on: Option<String>,
189
190    /// Record video to file
191    #[arg(long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
192    pub video_path: Option<PathBuf>,
193
194    /// Video output format
195    #[arg(long, default_value = "raw")]
196    pub video_format: VideoFormat,
197
198    /// Video frame rate multiplier or fixed value (1x, 2x, 3x, or a number like 60.0).
199    /// Multipliers sample the framebuffer more frequently, inserting half-finished frames.
200    /// Default is 1x (native PPU output rate).
201    #[arg(long, default_value = "1x")]
202    pub video_fps: String,
203
204    /// Video export mode: accurate or smooth.
205    /// - accurate: Encode at exact NES framerate (60.0988 fps or its multiple)
206    /// - smooth: Encode at exactly 60 fps (or its multiple), accepting slight timing drift
207    #[arg(long, default_value = "accurate")]
208    pub video_mode: VideoExportMode,
209
210    /// Video output resolution (native, 2x, 3x, 4x, 720p, 1080p, 4k, or WIDTHxHEIGHT)
211    #[arg(long, default_value = "native")]
212    pub video_scale: Option<String>,
213
214    /// Screen renderer to use for palette-to-RGB conversion.
215    /// Use --list-renderers to see available options.
216    #[arg(long)]
217    pub renderer: Option<String>,
218
219    /// List available screen renderers and exit
220    #[arg(long, default_value_t = false)]
221    pub list_renderers: bool,
222}
223
224/// Video export mode options
225#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq)]
226pub enum VideoExportMode {
227    /// Accurate mode: encode at exact NES framerate (60.0988 fps or its multiple)
228    #[default]
229    Accurate,
230    /// Smooth mode: encode at exactly 60 fps (or its multiple), accepting slight timing drift
231    Smooth,
232}
233
234/// Video format options
235#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq)]
236pub enum VideoFormat {
237    /// Raw RGBA frames (for piping to FFmpeg)
238    #[default]
239    Raw,
240    /// PPM image sequence
241    Ppm,
242    /// PNG image sequence
243    Png,
244    /// MP4 video
245    Mp4,
246}
247
248/// Execution control arguments
249#[derive(Args, Debug, Clone, Default)]
250pub struct ExecutionArgs {
251    /// Run for N master cycles
252    #[arg(long)]
253    pub cycles: Option<u128>,
254
255    /// Run for N frames
256    #[arg(short, long)]
257    pub frames: Option<u64>,
258
259    /// Run until specific opcode executes (hex, e.g., 0x02)
260    #[arg(long, value_parser = parse_hex_u8)]
261    pub until_opcode: Option<u8>,
262
263    /// Run until memory condition (e.g., 0x6000==0x80)
264    #[arg(long)]
265    pub until_mem: Option<Vec<String>>,
266
267    /// Run until HLT (illegal halt) instruction
268    #[arg(long, default_value_t = false)]
269    pub until_hlt: bool,
270
271    /// Enable instruction trace to file
272    #[arg(long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
273    pub trace: Option<PathBuf>,
274
275    /// Set breakpoint at PC address (can be specified multiple times)
276    #[arg(long, value_parser = parse_hex_u16, action = clap::ArgAction::Append)]
277    pub breakpoint: Vec<u16>,
278
279    /// Watch memory address for access (format: ADDR or ADDR:MODE where MODE is r/w/rw)
280    /// Stops execution when the CPU reads/writes the specified address.
281    /// Examples: 0x2002 (any access), 0x2002:r (reads only), 0x4016:w (writes only)
282    #[arg(long, action = clap::ArgAction::Append)]
283    pub watch_mem: Vec<String>,
284}
285
286/// Output control arguments
287#[derive(Args, Debug, Clone, Default)]
288pub struct OutputArgs {
289    /// Output file for memory dumps
290    #[arg(short, long, value_parser = value_parser!(PathBuf), value_hint = clap::ValueHint::FilePath)]
291    pub output: Option<PathBuf>,
292
293    /// Output format (hex, JSON, toml, binary)
294    #[arg(long, default_value = "hex")]
295    pub output_format: OutputFormat,
296
297    /// Output in JSON format (shorthand for --output-format JSON)
298    #[arg(long, default_value_t = false)]
299    pub json: bool,
300
301    /// Output in TOML format (shorthand for --output-format toml)
302    #[arg(long, default_value_t = false)]
303    pub toml: bool,
304
305    /// Output in binary format (shorthand for --output-format binary)
306    #[arg(long, default_value_t = false)]
307    pub binary: bool,
308}
309
310/// Output format options
311#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq)]
312pub enum OutputFormat {
313    /// Hexadecimal dump
314    #[default]
315    Hex,
316    /// JSON format
317    Json,
318    /// TOML format
319    Toml,
320    /// Raw binary
321    Binary,
322}
323
324impl std::str::FromStr for OutputFormat {
325    type Err = String;
326
327    fn from_str(s: &str) -> Result<Self, Self::Err> {
328        match s.to_lowercase().as_str() {
329            "hex" => Ok(OutputFormat::Hex),
330            "json" => Ok(OutputFormat::Json),
331            "toml" => Ok(OutputFormat::Toml),
332            "binary" => Ok(OutputFormat::Binary),
333            _ => Err(format!(
334                "Unknown output format: '{}'. Valid options: hex, json, toml, binary",
335                s
336            )),
337        }
338    }
339}
340
341impl std::str::FromStr for VideoFormat {
342    type Err = String;
343
344    fn from_str(s: &str) -> Result<Self, Self::Err> {
345        match s.to_lowercase().as_str() {
346            "raw" => Ok(VideoFormat::Raw),
347            "ppm" => Ok(VideoFormat::Ppm),
348            "png" => Ok(VideoFormat::Png),
349            "mp4" => Ok(VideoFormat::Mp4),
350            _ => Err(format!(
351                "Unknown video format: '{}'. Valid options: raw, ppm, png, mp4",
352                s
353            )),
354        }
355    }
356}
357
358impl std::str::FromStr for VideoExportMode {
359    type Err = String;
360
361    fn from_str(s: &str) -> Result<Self, Self::Err> {
362        match s.to_lowercase().as_str() {
363            "accurate" => Ok(VideoExportMode::Accurate),
364            "smooth" => Ok(VideoExportMode::Smooth),
365            _ => Err(format!(
366                "Unknown video export mode: '{}'. Valid options: accurate, smooth",
367                s
368            )),
369        }
370    }
371}
372
373impl std::str::FromStr for BuiltinPalette {
374    type Err = String;
375
376    fn from_str(s: &str) -> Result<Self, Self::Err> {
377        match s.to_lowercase().as_str() {
378            "2c02g" => Ok(BuiltinPalette::Nes2C02G),
379            "composite" => Ok(BuiltinPalette::Composite),
380            _ => Err(format!(
381                "Unknown palette: '{}'. Valid options: 2C02G, composite",
382                s
383            )),
384        }
385    }
386}
387
388/// Parse a hexadecimal u16 value (with or without 0x prefix)
389pub fn parse_hex_u16(s: &str) -> Result<u16, String> {
390    let s = s
391        .strip_prefix("0x")
392        .or_else(|| s.strip_prefix("0X"))
393        .unwrap_or(s);
394    u16::from_str_radix(s, 16).map_err(|e| format!("Invalid hex value '{}': {}", s, e))
395}
396
397/// Parse a hexadecimal u8 value (with or without 0x prefix)
398pub fn parse_hex_u8(s: &str) -> Result<u8, String> {
399    let s = s
400        .strip_prefix("0x")
401        .or_else(|| s.strip_prefix("0X"))
402        .unwrap_or(s);
403    u8::from_str_radix(s, 16).map_err(|e| format!("Invalid hex value '{}': {}", s, e))
404}
405
406impl OutputArgs {
407    /// Get the effective output format, considering shorthand flags
408    pub fn effective_format(&self) -> OutputFormat {
409        if self.json {
410            OutputFormat::Json
411        } else if self.toml {
412            OutputFormat::Toml
413        } else if self.binary {
414            OutputFormat::Binary
415        } else {
416            self.output_format
417        }
418    }
419}