ruvector_scipix/cli/commands/
config.rs1use 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#[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 Init {
21 #[arg(short, long, default_value = "scipix.toml")]
23 output: PathBuf,
24
25 #[arg(short, long)]
27 force: bool,
28 },
29
30 Validate {
32 #[arg(value_name = "FILE")]
34 file: PathBuf,
35 },
36
37 Show {
39 #[arg(value_name = "FILE")]
41 file: Option<PathBuf>,
42 },
43
44 Edit {
46 #[arg(value_name = "FILE")]
48 file: PathBuf,
49 },
50
51 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 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 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 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 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 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}