Skip to main content

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, &current);
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}