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