subx_cli/commands/config_command.rs
1//! Configuration management command implementation with hierarchical settings.
2//!
3//! This module provides comprehensive configuration management capabilities
4//! through the `config` subcommand, enabling users to view, modify, and manage
5//! application settings across multiple configuration categories and sources.
6//! It supports hierarchical configuration with validation and type safety.
7//!
8//! # Configuration Architecture
9//!
10//! ## Configuration Categories
11//! - **General**: Basic application behavior and preferences
12//! - **AI Settings**: AI service providers, models, and API configuration
13//! - **Audio Processing**: Audio analysis and synchronization parameters
14//! - **Format Options**: Default output formats and conversion settings
15//! - **Cache Management**: Caching behavior and storage configuration
16//! - **Sync Settings**: Subtitle timing and synchronization options
17//!
18//! ## Configuration Sources (Priority Order)
19//! 1. **Command-line Arguments**: Highest priority, session-specific
20//! 2. **Environment Variables**: Runtime configuration overrides
21//! 3. **User Configuration**: Personal settings in user config directory
22//! 4. **Project Configuration**: Local project-specific settings
23//! 5. **System Configuration**: Global system-wide defaults
24//! 6. **Built-in Defaults**: Application default values
25//!
26//! # Supported Operations
27//!
28//! ## Set Operation
29//! - **Type Validation**: Ensure values match expected data types
30//! - **Range Checking**: Validate numeric values are within bounds
31//! - **Format Verification**: Check string values follow required patterns
32//! - **Dependency Validation**: Verify related settings are compatible
33//! - **Backup Creation**: Preserve previous values for rollback
34//!
35//! ## Get Operation
36//! - **Value Display**: Show current effective value
37//! - **Source Identification**: Indicate where value originates
38//! - **Type Information**: Display expected data type and constraints
39//! - **Default Comparison**: Show difference from built-in defaults
40//! - **Metadata Display**: Include help text and validation rules
41//!
42//! ## List Operation
43//! - **Categorized Display**: Group settings by functional area
44//! - **Source Indicators**: Show which settings are customized
45//! - **Value Formatting**: Display values in appropriate format
46//! - **Filter Options**: Support for category and status filtering
47//! - **Export Capability**: Generate configuration for sharing
48//!
49//! ## Reset Operation
50//! - **Backup Creation**: Automatic backup before reset
51//! - **Selective Reset**: Option to reset specific categories
52//! - **Confirmation Process**: Interactive confirmation for safety
53//! - **Recovery Information**: Instructions for backup restoration
54//!
55//! # Configuration Keys
56//!
57//! ## General Settings
58//! ```text
59//! general.enable_progress_bar # Boolean: Show progress indicators
60//! general.backup_enabled # Boolean: Automatic file backups
61//! general.task_timeout_seconds # Integer: Operation timeout in seconds
62//! ```
63//!
64//! ## AI Configuration
65//! ```text
66//! ai.provider # String: AI service provider
67//! ai.api_key # String: OpenAI API authentication
68//! ai.model # String: GPT model selection
69//! ai.max_tokens # Integer: Maximum response length
70//! ai.base_url # String: API endpoint URL
71//! ai.max_sample_length # Integer: Text sample size for analysis
72//! ai.temperature # Float: Response randomness control
73//! ai.retry_attempts # Integer: API request retry count
74//! ai.retry_delay_ms # Integer: Retry delay in milliseconds
75//! ```
76//!
77//! ## Audio Processing
78//! ```text
79//! audio.max_offset_seconds # Float: Maximum sync offset range
80//! audio.correlation_threshold # Float: Minimum correlation for sync
81//! audio.dialogue_threshold # Float: Speech detection sensitivity
82//! audio.min_dialogue_duration_ms # Integer: Minimum speech segment length
83//! audio.enable_dialogue_detection # Boolean: Advanced audio analysis
84//! ```
85//!
86//! # Examples
87//!
88//! ```rust,ignore
89//! use subx_cli::cli::{ConfigArgs, ConfigAction};
90//! use subx_cli::commands::config_command;
91//!
92//! // Set AI provider
93//! let set_args = ConfigArgs {
94//! action: ConfigAction::Set {
95//! key: "ai.provider".to_string(),
96//! value: "openai".to_string(),
97//! },
98//! };
99//! config_command::execute(set_args).await?;
100//!
101//! // Get current AI model
102//! let get_args = ConfigArgs {
103//! action: ConfigAction::Get {
104//! key: "ai.openai.model".to_string(),
105//! },
106//! };
107//! config_command::execute(get_args).await?;
108//! ```
109
110use crate::cli::output::{active_mode, emit_success, emit_success_with_warnings};
111use crate::cli::{ConfigAction, ConfigArgs};
112use crate::config::{ConfigService, mask_sensitive_value};
113use crate::error::{SubXError, SubXResult};
114use serde::Serialize;
115use serde_json::{Map, Value};
116
117/// JSON payload for `config set` — the key/value that was just persisted.
118#[derive(Debug, Serialize)]
119pub struct ConfigSetPayload<'a> {
120 /// Configuration key that was modified.
121 pub key: &'a str,
122 /// New value (sensitive values are masked).
123 pub value: String,
124}
125
126/// JSON payload for `config get`/`list`/`reset` — the resolved
127/// configuration object (or a single-key projection for `get`).
128#[derive(Debug, Serialize)]
129pub struct ConfigPayload {
130 /// Resolved configuration map.
131 pub config: Value,
132}
133
134fn build_config_value(config_service: &dyn ConfigService) -> SubXResult<Value> {
135 let mut config = config_service.get_config()?;
136 if let Some(key) = config.ai.api_key.as_ref() {
137 config.ai.api_key = Some(mask_sensitive_value("ai.api_key", key));
138 }
139 serde_json::to_value(&config)
140 .map_err(|e| SubXError::config(format!("JSON serialization error: {e}")))
141}
142
143/// Build a JSON-friendly view of the configuration using the *tolerant*
144/// load path so that an existing strict-invalid `config.toml` does not
145/// prevent `config get`/`list` from inspecting it.
146///
147/// The returned vector contains zero or more advisory warnings: it is
148/// non-empty iff the on-disk configuration fails cross-section
149/// validation. Callers MUST surface these warnings either via
150/// [`emit_success_with_warnings`] (JSON mode) or as stderr lines (text
151/// mode) so the user can see *why* the on-disk file is currently
152/// invalid before issuing the repair `config set` mutation.
153fn build_config_value_for_repair(
154 config_service: &dyn ConfigService,
155) -> SubXResult<(Value, Vec<String>)> {
156 let mut config = config_service.load_for_repair()?;
157 let warnings = collect_strict_validation_warnings(&config);
158 if let Some(key) = config.ai.api_key.as_ref() {
159 config.ai.api_key = Some(mask_sensitive_value("ai.api_key", key));
160 }
161 let value = serde_json::to_value(&config)
162 .map_err(|e| SubXError::config(format!("JSON serialization error: {e}")))?;
163 Ok((value, warnings))
164}
165
166/// Run cross-section validation on `config` purely for diagnostic
167/// purposes. Returns one user-friendly warning string per validation
168/// failure; for the typical "single validator error" case the vector
169/// has length 1. The returned vector is empty when `config` is
170/// strict-valid.
171fn collect_strict_validation_warnings(config: &crate::config::Config) -> Vec<String> {
172 match crate::config::validator::validate_config(config) {
173 Ok(()) => Vec::new(),
174 Err(err) => {
175 vec![format!("configuration is currently invalid: {err}")]
176 }
177 }
178}
179
180async fn run_config_action(args: ConfigArgs, config_service: &dyn ConfigService) -> SubXResult<()> {
181 let mode = active_mode();
182 let json_mode = mode.is_json();
183
184 match args.action {
185 ConfigAction::Set { key, value } => {
186 config_service.set_config_value(&key, &value)?;
187 let masked = mask_sensitive_value(&key, &value);
188 if json_mode {
189 emit_success(
190 mode,
191 "config",
192 ConfigSetPayload {
193 key: &key,
194 value: masked,
195 },
196 );
197 } else {
198 println!("✓ Configuration '{key}' set to '{masked}'");
199 if let Ok(current) = config_service.get_config_value(&key) {
200 let masked_current = mask_sensitive_value(&key, ¤t);
201 println!(" Current value: {masked_current}");
202 }
203 if let Ok(path) = config_service.get_config_file_path() {
204 println!(" Saved to: {}", path.display());
205 }
206 }
207 }
208 ConfigAction::Get { key } => {
209 // Tolerant read: load the file directly so users can
210 // inspect a strict-invalid configuration.
211 let config = config_service.load_for_repair()?;
212 let warnings = collect_strict_validation_warnings(&config);
213 let value = crate::config::service::read_config_value_from(&config, &key)?;
214 let masked = mask_sensitive_value(&key, &value);
215 if json_mode {
216 let mut obj = Map::new();
217 obj.insert(key.clone(), Value::String(masked));
218 emit_success_with_warnings(
219 mode,
220 "config",
221 ConfigPayload {
222 config: Value::Object(obj),
223 },
224 warnings,
225 );
226 } else {
227 println!("{masked}");
228 for warning in &warnings {
229 eprintln!("warning: {warning}");
230 }
231 }
232 }
233 ConfigAction::List => {
234 if json_mode {
235 let (config_value, warnings) = build_config_value_for_repair(config_service)?;
236 let payload = ConfigPayload {
237 config: config_value,
238 };
239 emit_success_with_warnings(mode, "config", payload, warnings);
240 } else {
241 let mut config = config_service.load_for_repair()?;
242 let warnings = collect_strict_validation_warnings(&config);
243 if let Ok(path) = config_service.get_config_file_path() {
244 println!("# Configuration file path: {}\n", path.display());
245 }
246 if let Some(key) = config.ai.api_key.as_ref() {
247 config.ai.api_key = Some(mask_sensitive_value("ai.api_key", key));
248 }
249 println!(
250 "{}",
251 toml::to_string_pretty(&config)
252 .map_err(|e| SubXError::config(format!("TOML serialization error: {e}")))?
253 );
254 for warning in &warnings {
255 eprintln!("warning: {warning}");
256 }
257 }
258 }
259 ConfigAction::Reset => {
260 config_service.reset_to_defaults()?;
261 if json_mode {
262 let payload = ConfigPayload {
263 config: build_config_value(config_service)?,
264 };
265 emit_success(mode, "config", payload);
266 } else {
267 println!("Configuration reset to default values");
268 if let Ok(path) = config_service.get_config_file_path() {
269 println!("Default configuration saved to: {}", path.display());
270 }
271 }
272 }
273 }
274 Ok(())
275}
276
277/// Execute configuration management operations with validation and type safety.
278///
279/// This function provides the main entry point for all configuration management
280/// operations, including setting values, retrieving current configuration,
281/// listing all settings, and resetting to defaults. It includes comprehensive
282/// validation, error handling, and user-friendly output formatting.
283///
284/// # Operation Workflow
285///
286/// ## Set Operation
287/// 1. **Configuration Loading**: Load current configuration from all sources
288/// 2. **Key Validation**: Verify configuration key exists and is writable
289/// 3. **Value Parsing**: Convert string value to appropriate data type
290/// 4. **Constraint Checking**: Validate value meets all requirements
291/// 5. **Dependency Verification**: Check related settings compatibility
292/// 6. **Backup Creation**: Save current value for potential rollback
293/// 7. **Value Application**: Update configuration with new value
294/// 8. **Persistence**: Save updated configuration to appropriate file
295/// 9. **Confirmation**: Display success message with applied value
296///
297/// ## Get Operation
298/// 1. **Configuration Loading**: Load current effective configuration
299/// 2. **Key Resolution**: Locate requested configuration setting
300/// 3. **Source Identification**: Determine where value originates
301/// 4. **Value Formatting**: Format value for appropriate display
302/// 5. **Metadata Retrieval**: Gather type and constraint information
303/// 6. **Output Generation**: Create comprehensive information display
304///
305/// ## List Operation
306/// 1. **Configuration Loading**: Load all configuration settings
307/// 2. **Categorization**: Group settings by functional area
308/// 3. **Source Analysis**: Identify customized vs. default values
309/// 4. **Formatting**: Prepare values for tabular display
310/// 5. **Output Generation**: Create organized configuration overview
311///
312/// ## Reset Operation
313/// 1. **Current State Backup**: Create timestamped configuration backup
314/// 2. **User Confirmation**: Interactive confirmation for destructive operation
315/// 3. **Default Restoration**: Replace all settings with built-in defaults
316/// 4. **Validation**: Verify reset configuration is valid
317/// 5. **Persistence**: Save default configuration to user config file
318/// 6. **Confirmation**: Display reset completion and backup location
319///
320/// # Type System Integration
321///
322/// The configuration system provides strong typing with automatic conversion:
323/// - **Boolean Values**: "true", "false", "1", "0", "yes", "no"
324/// - **Integer Values**: Decimal notation with range validation
325/// - **Float Values**: Decimal notation with precision preservation
326/// - **String Values**: UTF-8 text with format validation where applicable
327/// - **Array Values**: JSON array format for complex configuration
328///
329/// # Validation Framework
330///
331/// Each configuration setting includes comprehensive validation:
332/// - **Type Constraints**: Must match expected data type
333/// - **Range Limits**: Numeric values within acceptable bounds
334/// - **Format Requirements**: String values matching required patterns
335/// - **Dependency Rules**: Related settings must be compatible
336/// - **Security Checks**: Sensitive values properly protected
337///
338/// # Arguments
339///
340/// * `args` - Configuration command arguments containing the specific
341/// operation to perform (set, get, list, or reset) along with any
342/// required parameters such as key names and values.
343///
344/// # Returns
345///
346/// Returns `Ok(())` on successful operation completion, or an error describing:
347/// - Configuration loading or parsing failures
348/// - Invalid configuration keys or malformed key paths
349/// - Type conversion or validation errors
350/// - File system access problems during persistence
351/// - User cancellation of destructive operations
352///
353/// # Error Categories
354///
355/// ## Configuration Errors
356/// - **Invalid Key**: Specified configuration key does not exist
357/// - **Type Mismatch**: Value cannot be converted to expected type
358/// - **Range Error**: Numeric value outside acceptable range
359/// - **Format Error**: String value doesn't match required pattern
360/// - **Dependency Error**: Value conflicts with related settings
361///
362/// ## System Errors
363/// - **File Access**: Cannot read or write configuration files
364/// - **Permission Error**: Insufficient privileges for operation
365/// - **Disk Space**: Insufficient space for configuration persistence
366/// - **Corruption**: Configuration file is damaged or invalid
367///
368/// # Security Considerations
369///
370/// - **Sensitive Values**: API keys and credentials are properly masked in output
371/// - **File Permissions**: Configuration files created with appropriate permissions
372/// - **Backup Protection**: Backup files inherit security settings
373/// - **Validation**: All input values sanitized and validated
374///
375/// # Examples
376///
377/// ```rust,ignore
378/// use subx_cli::cli::{ConfigArgs, ConfigAction};
379/// use subx_cli::commands::config_command;
380///
381/// // Configure AI service with API key
382/// let ai_setup = ConfigArgs {
383/// action: ConfigAction::Set {
384/// key: "ai.openai.api_key".to_string(),
385/// value: "sk-1234567890abcdef".to_string(),
386/// },
387/// };
388/// config_command::execute(ai_setup).await?;
389///
390/// // Adjust audio processing sensitivity
391/// let audio_tuning = ConfigArgs {
392/// action: ConfigAction::Set {
393/// key: "audio.correlation_threshold".to_string(),
394/// value: "0.85".to_string(),
395/// },
396/// };
397/// config_command::execute(audio_tuning).await?;
398///
399/// // View complete configuration
400/// let view_all = ConfigArgs {
401/// action: ConfigAction::List,
402/// };
403/// config_command::execute(view_all).await?;
404///
405/// // Reset to clean state
406/// let reset_config = ConfigArgs {
407/// action: ConfigAction::Reset,
408/// };
409/// config_command::execute(reset_config).await?;
410/// ```
411pub async fn execute(args: ConfigArgs, config_service: &dyn ConfigService) -> SubXResult<()> {
412 run_config_action(args, config_service).await
413}
414
415/// Execute configuration management command with injected configuration service.
416///
417/// This function provides the new dependency injection interface for the config command,
418/// accepting a configuration service instead of loading configuration globally.
419///
420/// # Arguments
421///
422/// * `args` - Configuration command arguments
423/// * `config_service` - Configuration service providing access to settings
424///
425/// # Returns
426///
427/// Returns `Ok(())` on successful completion, or an error if the operation fails.
428pub async fn execute_with_config(
429 args: ConfigArgs,
430 config_service: std::sync::Arc<dyn ConfigService>,
431) -> SubXResult<()> {
432 run_config_action(args, config_service.as_ref()).await
433}