ruvector_scipix/cli/commands/
config.rs

1use anyhow::{Context, Result};
2use clap::{Args, Subcommand};
3use dialoguer::{theme::ColorfulTheme, Confirm, Input};
4use std::path::PathBuf;
5use tracing::info;
6
7use crate::cli::Cli;
8use super::OcrConfig;
9
10/// Manage configuration
11#[derive(Args, Debug, Clone)]
12pub struct ConfigArgs {
13    #[command(subcommand)]
14    pub command: ConfigCommand,
15}
16
17#[derive(Subcommand, Debug, Clone)]
18pub enum ConfigCommand {
19    /// Generate default configuration file
20    Init {
21        /// Output path for config file
22        #[arg(short, long, default_value = "scipix.toml")]
23        output: PathBuf,
24
25        /// Overwrite existing file
26        #[arg(short, long)]
27        force: bool,
28    },
29
30    /// Validate configuration file
31    Validate {
32        /// Path to config file to validate
33        #[arg(value_name = "FILE")]
34        file: PathBuf,
35    },
36
37    /// Show current configuration
38    Show {
39        /// Path to config file (default: from --config or scipix.toml)
40        #[arg(value_name = "FILE")]
41        file: Option<PathBuf>,
42    },
43
44    /// Edit configuration interactively
45    Edit {
46        /// Path to config file to edit
47        #[arg(value_name = "FILE")]
48        file: PathBuf,
49    },
50
51    /// Get configuration directory path
52    Path,
53}
54
55pub async fn execute(args: ConfigArgs, cli: &Cli) -> Result<()> {
56    match args.command {
57        ConfigCommand::Init { output, force } => {
58            init_config(&output, force)?;
59        }
60        ConfigCommand::Validate { file } => {
61            validate_config(&file)?;
62        }
63        ConfigCommand::Show { file } => {
64            show_config(file.or(cli.config.clone()))?;
65        }
66        ConfigCommand::Edit { file } => {
67            edit_config(&file)?;
68        }
69        ConfigCommand::Path => {
70            show_config_path()?;
71        }
72    }
73
74    Ok(())
75}
76
77fn init_config(output: &PathBuf, force: bool) -> Result<()> {
78    if output.exists() && !force {
79        anyhow::bail!(
80            "Config file already exists: {} (use --force to overwrite)",
81            output.display()
82        );
83    }
84
85    let config = OcrConfig::default();
86    let toml = toml::to_string_pretty(&config)
87        .context("Failed to serialize config")?;
88
89    std::fs::write(output, toml)
90        .context("Failed to write config file")?;
91
92    info!("Configuration file created: {}", output.display());
93    println!("āœ“ Created configuration file: {}", output.display());
94    println!("\nTo use this config, run:");
95    println!("  scipix-cli --config {} <command>", output.display());
96    println!("\nOr set environment variable:");
97    println!("  export MATHPIX_CONFIG={}", output.display());
98
99    Ok(())
100}
101
102fn validate_config(file: &PathBuf) -> Result<()> {
103    if !file.exists() {
104        anyhow::bail!("Config file not found: {}", file.display());
105    }
106
107    let content = std::fs::read_to_string(file)
108        .context("Failed to read config file")?;
109
110    let config: OcrConfig = toml::from_str(&content)
111        .context("Failed to parse config file")?;
112
113    // Validate configuration values
114    if config.min_confidence < 0.0 || config.min_confidence > 1.0 {
115        anyhow::bail!("min_confidence must be between 0.0 and 1.0");
116    }
117
118    if config.max_image_size == 0 {
119        anyhow::bail!("max_image_size must be greater than 0");
120    }
121
122    if config.supported_extensions.is_empty() {
123        anyhow::bail!("supported_extensions cannot be empty");
124    }
125
126    println!("āœ“ Configuration is valid");
127    println!("\nSettings:");
128    println!("  Min confidence: {}", config.min_confidence);
129    println!("  Max image size: {} bytes", config.max_image_size);
130    println!("  Supported extensions: {}", config.supported_extensions.join(", "));
131
132    if let Some(endpoint) = &config.api_endpoint {
133        println!("  API endpoint: {}", endpoint);
134    }
135
136    Ok(())
137}
138
139fn show_config(file: Option<PathBuf>) -> Result<()> {
140    let config_path = file.unwrap_or_else(|| {
141        PathBuf::from("scipix.toml")
142    });
143
144    if !config_path.exists() {
145        println!("No configuration file found.");
146        println!("\nCreate one with:");
147        println!("  scipix-cli config init");
148        return Ok(());
149    }
150
151    let content = std::fs::read_to_string(&config_path)
152        .context("Failed to read config file")?;
153
154    println!("Configuration from: {}\n", config_path.display());
155    println!("{}", content);
156
157    Ok(())
158}
159
160fn edit_config(file: &PathBuf) -> Result<()> {
161    if !file.exists() {
162        anyhow::bail!(
163            "Config file not found: {} (use 'config init' to create)",
164            file.display()
165        );
166    }
167
168    let content = std::fs::read_to_string(file)
169        .context("Failed to read config file")?;
170
171    let mut config: OcrConfig = toml::from_str(&content)
172        .context("Failed to parse config file")?;
173
174    let theme = ColorfulTheme::default();
175
176    println!("Interactive Configuration Editor\n");
177
178    // Edit min_confidence
179    config.min_confidence = Input::with_theme(&theme)
180        .with_prompt("Minimum confidence threshold (0.0-1.0)")
181        .default(config.min_confidence)
182        .validate_with(|v: &f64| {
183            if *v >= 0.0 && *v <= 1.0 {
184                Ok(())
185            } else {
186                Err("Value must be between 0.0 and 1.0")
187            }
188        })
189        .interact_text()
190        .context("Failed to read input")?;
191
192    // Edit max_image_size
193    let max_size_mb = config.max_image_size / (1024 * 1024);
194    let new_size_mb: usize = Input::with_theme(&theme)
195        .with_prompt("Maximum image size (MB)")
196        .default(max_size_mb)
197        .interact_text()
198        .context("Failed to read input")?;
199    config.max_image_size = new_size_mb * 1024 * 1024;
200
201    // Edit API endpoint
202    if config.api_endpoint.is_some() {
203        let edit_endpoint = Confirm::with_theme(&theme)
204            .with_prompt("Edit API endpoint?")
205            .default(false)
206            .interact()
207            .context("Failed to read input")?;
208
209        if edit_endpoint {
210            let endpoint: String = Input::with_theme(&theme)
211                .with_prompt("API endpoint URL")
212                .allow_empty(true)
213                .interact_text()
214                .context("Failed to read input")?;
215
216            config.api_endpoint = if endpoint.is_empty() {
217                None
218            } else {
219                Some(endpoint)
220            };
221        }
222    } else {
223        let add_endpoint = Confirm::with_theme(&theme)
224            .with_prompt("Add API endpoint?")
225            .default(false)
226            .interact()
227            .context("Failed to read input")?;
228
229        if add_endpoint {
230            let endpoint: String = Input::with_theme(&theme)
231                .with_prompt("API endpoint URL")
232                .interact_text()
233                .context("Failed to read input")?;
234
235            config.api_endpoint = Some(endpoint);
236        }
237    }
238
239    // Save configuration
240    let save = Confirm::with_theme(&theme)
241        .with_prompt("Save changes?")
242        .default(true)
243        .interact()
244        .context("Failed to read input")?;
245
246    if save {
247        let toml = toml::to_string_pretty(&config)
248            .context("Failed to serialize config")?;
249
250        std::fs::write(file, toml)
251            .context("Failed to write config file")?;
252
253        println!("\nāœ“ Configuration saved to: {}", file.display());
254    } else {
255        println!("\nChanges discarded.");
256    }
257
258    Ok(())
259}
260
261fn show_config_path() -> Result<()> {
262    if let Some(config_dir) = dirs::config_dir() {
263        let app_config = config_dir.join("scipix");
264        println!("Default config directory: {}", app_config.display());
265
266        if !app_config.exists() {
267            println!("\nDirectory does not exist. Create it with:");
268            println!("  mkdir -p {}", app_config.display());
269        }
270    } else {
271        println!("Could not determine config directory");
272    }
273
274    println!("\nYou can also use a custom config file:");
275    println!("  scipix-cli --config /path/to/config.toml <command>");
276    println!("\nOr set environment variable:");
277    println!("  export MATHPIX_CONFIG=/path/to/config.toml");
278
279    Ok(())
280}