vx_core/
config_figment.rs

1//! Figment-based configuration system for vx
2//! Leverages the excellent figment crate for layered configuration
3//! Supports reading from existing project configuration files
4
5use crate::{Result, VxError};
6use figment::{
7    providers::{Env, Format, Serialized, Toml},
8    Figment,
9};
10use serde::{Deserialize, Serialize};
11use serde_json::Value as JsonValue;
12use std::collections::HashMap;
13use std::fs;
14use std::path::PathBuf;
15
16/// Main vx configuration structure
17#[derive(Debug, Serialize, Deserialize, Default)]
18pub struct VxConfig {
19    /// Global default settings
20    pub defaults: DefaultConfig,
21    /// Tool-specific configurations
22    pub tools: HashMap<String, ToolConfig>,
23    /// Registry configurations
24    pub registries: HashMap<String, RegistryConfig>,
25}
26
27/// Default configuration settings
28#[derive(Debug, Serialize, Deserialize)]
29pub struct DefaultConfig {
30    /// Automatically install missing tools
31    pub auto_install: bool,
32    /// Check for updates periodically
33    pub check_updates: bool,
34    /// Update check interval
35    pub update_interval: String,
36    /// Default registry to use
37    pub default_registry: String,
38    /// Whether to fall back to builtin configuration
39    pub fallback_to_builtin: bool,
40}
41
42impl Default for DefaultConfig {
43    fn default() -> Self {
44        Self {
45            auto_install: true,
46            check_updates: true,
47            update_interval: "24h".to_string(),
48            default_registry: "official".to_string(),
49            fallback_to_builtin: true,
50        }
51    }
52}
53
54/// Tool-specific configuration
55#[derive(Debug, Serialize, Deserialize, Clone)]
56pub struct ToolConfig {
57    /// Preferred version (latest, lts, specific version)
58    pub version: Option<String>,
59    /// Installation method preference
60    pub install_method: Option<String>,
61    /// Registry to use for this tool
62    pub registry: Option<String>,
63    /// Custom download sources
64    pub custom_sources: Option<HashMap<String, String>>,
65}
66
67/// Registry configuration
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct RegistryConfig {
70    /// Registry name
71    pub name: String,
72    /// Base URL for the registry
73    pub base_url: String,
74    /// API URL (optional)
75    pub api_url: Option<String>,
76    /// Authentication token (optional)
77    pub auth_token: Option<String>,
78    /// Registry priority (higher = more preferred)
79    pub priority: i32,
80    /// Whether this registry is enabled
81    pub enabled: bool,
82}
83
84/// Project configuration detection result
85#[derive(Debug, Clone)]
86pub struct ProjectInfo {
87    pub project_type: ProjectType,
88    pub config_file: PathBuf,
89    pub tool_versions: HashMap<String, String>,
90}
91
92/// Supported project types
93#[derive(Debug, Clone, PartialEq)]
94pub enum ProjectType {
95    Python,  // pyproject.toml
96    Rust,    // Cargo.toml
97    Node,    // package.json
98    Go,      // go.mod
99    Mixed,   // Multiple project types
100    Unknown, // No recognized project files
101}
102
103/// Configuration status for diagnostics
104#[derive(Debug, Clone)]
105pub struct ConfigStatus {
106    pub layers: Vec<LayerInfo>,
107    pub available_tools: Vec<String>,
108    pub fallback_enabled: bool,
109    pub project_info: Option<ProjectInfo>,
110}
111
112/// Information about a configuration layer
113#[derive(Debug, Clone)]
114pub struct LayerInfo {
115    pub name: String,
116    pub available: bool,
117    pub priority: i32,
118}
119
120impl ConfigStatus {
121    /// Get a summary of the configuration status
122    pub fn summary(&self) -> String {
123        let active_layers: Vec<&str> = self
124            .layers
125            .iter()
126            .filter(|l| l.available)
127            .map(|l| l.name.as_str())
128            .collect();
129
130        format!(
131            "Configuration layers: {} | Tools: {} | Fallback: {}",
132            active_layers.join(", "),
133            self.available_tools.len(),
134            if self.fallback_enabled {
135                "enabled"
136            } else {
137                "disabled"
138            }
139        )
140    }
141
142    /// Check if the configuration is healthy
143    pub fn is_healthy(&self) -> bool {
144        // At least one layer should be available
145        self.layers.iter().any(|l| l.available) && !self.available_tools.is_empty()
146    }
147}
148
149/// Configuration manager using Figment
150pub struct FigmentConfigManager {
151    figment: Figment,
152    config: VxConfig,
153    project_info: Option<ProjectInfo>,
154}
155
156impl FigmentConfigManager {
157    /// Create a new configuration manager with full layered configuration
158    pub fn new() -> Result<Self> {
159        let project_info = Self::detect_project_info()?;
160        let figment = Self::build_figment(&project_info)?;
161        let config = figment.extract().map_err(|e| VxError::ConfigError {
162            message: format!("Failed to extract configuration: {}", e),
163        })?;
164
165        Ok(Self {
166            figment,
167            config,
168            project_info,
169        })
170    }
171
172    /// Create a minimal configuration manager (builtin defaults only)
173    pub fn minimal() -> Result<Self> {
174        let figment = Figment::from(Serialized::defaults(VxConfig::default()));
175        let config = figment.extract().map_err(|e| VxError::ConfigError {
176            message: format!("Failed to extract minimal configuration: {}", e),
177        })?;
178
179        Ok(Self {
180            figment,
181            config,
182            project_info: None,
183        })
184    }
185
186    /// Get tool configuration
187    pub fn get_tool_config(&self, tool_name: &str) -> Option<&ToolConfig> {
188        self.config.tools.get(tool_name)
189    }
190
191    /// Get available tools (from configuration + builtin)
192    pub fn get_available_tools(&self) -> Vec<String> {
193        let mut tools = std::collections::HashSet::new();
194
195        // Add configured tools
196        for tool in self.config.tools.keys() {
197            tools.insert(tool.clone());
198        }
199
200        // Add builtin tools if fallback is enabled
201        if self.config.defaults.fallback_to_builtin {
202            for tool in &["uv", "node", "go", "rust"] {
203                tools.insert(tool.to_string());
204            }
205        }
206
207        let mut result: Vec<String> = tools.into_iter().collect();
208        result.sort();
209        result
210    }
211
212    /// Check if a tool is supported
213    pub fn supports_tool(&self, tool_name: &str) -> bool {
214        // Check if configured
215        if self.config.tools.contains_key(tool_name) {
216            return true;
217        }
218
219        // Check builtin if fallback enabled
220        if self.config.defaults.fallback_to_builtin {
221            return ["uv", "node", "go", "rust"].contains(&tool_name);
222        }
223
224        false
225    }
226
227    /// Get the current configuration
228    pub fn config(&self) -> &VxConfig {
229        &self.config
230    }
231
232    /// Get project information
233    pub fn project_info(&self) -> &Option<ProjectInfo> {
234        &self.project_info
235    }
236
237    /// Get the underlying figment for advanced usage
238    pub fn figment(&self) -> &Figment {
239        &self.figment
240    }
241
242    /// Reload configuration
243    pub fn reload(&mut self) -> Result<()> {
244        self.project_info = Self::detect_project_info()?;
245        self.figment = Self::build_figment(&self.project_info)?;
246        self.config = self.figment.extract().map_err(|e| VxError::ConfigError {
247            message: format!("Failed to reload configuration: {}", e),
248        })?;
249        Ok(())
250    }
251
252    /// Get download URL for a tool and version
253    pub fn get_download_url(&self, tool_name: &str, version: &str) -> Result<String> {
254        // First try to get from configuration
255        if let Some(tool_config) = self.config.tools.get(tool_name) {
256            if let Some(custom_sources) = &tool_config.custom_sources {
257                if let Some(url_template) = custom_sources.get("default") {
258                    return Ok(self.expand_url_template(url_template, tool_name, version));
259                }
260            }
261        }
262
263        // Fall back to builtin URL builders
264        if self.config.defaults.fallback_to_builtin {
265            match tool_name {
266                "node" => {
267                    crate::NodeUrlBuilder::download_url(version).ok_or_else(|| VxError::Other {
268                        message: format!("No download URL available for {} {}", tool_name, version),
269                    })
270                }
271                "go" => crate::GoUrlBuilder::download_url(version).ok_or_else(|| VxError::Other {
272                    message: format!("No download URL available for {} {}", tool_name, version),
273                }),
274                _ => Err(VxError::Other {
275                    message: format!("Tool {} not supported", tool_name),
276                }),
277            }
278        } else {
279            Err(VxError::Other {
280                message: format!("Tool {} not configured and fallback disabled", tool_name),
281            })
282        }
283    }
284
285    /// Expand URL template with variables
286    fn expand_url_template(&self, template: &str, tool_name: &str, version: &str) -> String {
287        let platform = crate::Platform::current();
288        let (os, arch) = match tool_name {
289            "node" => platform
290                .node_platform_string()
291                .unwrap_or(("linux".to_string(), "x64".to_string())),
292            "go" => platform
293                .go_platform_string()
294                .unwrap_or(("linux".to_string(), "amd64".to_string())),
295            _ => ("linux".to_string(), "x64".to_string()),
296        };
297        let ext = platform.archive_extension();
298
299        template
300            .replace("{tool}", tool_name)
301            .replace("{version}", version)
302            .replace("{platform}", &os)
303            .replace("{arch}", &arch)
304            .replace("{ext}", ext)
305    }
306
307    /// Validate the current configuration
308    pub fn validate(&self) -> Result<Vec<String>> {
309        let mut warnings = Vec::new();
310
311        // Validate tool configurations
312        for (tool_name, tool_config) in &self.config.tools {
313            if let Some(version) = &tool_config.version {
314                if version.is_empty() {
315                    warnings.push(format!("Tool '{}' has empty version", tool_name));
316                }
317            }
318
319            if let Some(custom_sources) = &tool_config.custom_sources {
320                for (source_name, url) in custom_sources {
321                    if !url.contains("{version}") && !url.contains("{tool}") {
322                        warnings.push(format!(
323                            "Tool '{}' source '{}' URL may be missing version/tool placeholders",
324                            tool_name, source_name
325                        ));
326                    }
327                }
328            }
329        }
330
331        // Validate registry configurations
332        for (registry_name, registry_config) in &self.config.registries {
333            if registry_config.base_url.is_empty() {
334                warnings.push(format!("Registry '{}' has empty base URL", registry_name));
335            }
336        }
337
338        // Validate default settings
339        if self.config.defaults.update_interval.is_empty() {
340            warnings.push("Update interval is empty".to_string());
341        }
342
343        Ok(warnings)
344    }
345
346    /// Initialize a new .vx.toml configuration file in the current directory
347    pub fn init_project_config(
348        &self,
349        tools: Option<HashMap<String, String>>,
350        interactive: bool,
351    ) -> Result<()> {
352        let config_path = std::env::current_dir()
353            .map_err(|e| VxError::Other {
354                message: format!("Failed to get current directory: {}", e),
355            })?
356            .join(".vx.toml");
357
358        if config_path.exists() {
359            return Err(VxError::Other {
360                message: "Configuration file .vx.toml already exists".to_string(),
361            });
362        }
363
364        let mut project_config = crate::venv::ProjectConfig::default();
365
366        // Add provided tools or detect from project
367        if let Some(tools) = tools {
368            project_config.tools = tools;
369        } else if interactive {
370            // In interactive mode, we could prompt for tools
371            // For now, just detect from existing project files
372            if let Some(project_info) = &self.project_info {
373                project_config.tools = project_info.tool_versions.clone();
374            }
375        }
376
377        // Set sensible defaults
378        project_config.settings.auto_install = true;
379        project_config.settings.cache_duration = "7d".to_string();
380
381        // Generate TOML content
382        let toml_content = toml::to_string_pretty(&project_config).map_err(|e| VxError::Other {
383            message: format!("Failed to serialize configuration: {}", e),
384        })?;
385
386        // Add header comment
387        let header = r#"# VX Project Configuration
388# This file defines the tools and versions required for this project.
389# Run 'vx sync' to install all required tools.
390
391"#;
392
393        let full_content = format!("{}{}", header, toml_content);
394
395        // Write to file
396        std::fs::write(&config_path, full_content).map_err(|e| VxError::Other {
397            message: format!("Failed to write .vx.toml: {}", e),
398        })?;
399
400        Ok(())
401    }
402
403    /// Sync project configuration - install all required tools
404    pub async fn sync_project(&self, force: bool) -> Result<Vec<String>> {
405        let mut installed_tools = Vec::new();
406
407        // Load project configuration
408        let venv_manager = crate::VenvManager::new()?;
409        let project_config = venv_manager.load_project_config()?;
410
411        if let Some(config) = project_config {
412            for (tool_name, version) in &config.tools {
413                // Check if tool is already installed
414                let env = crate::VxEnvironment::new()?;
415                let is_installed = env.is_version_installed(tool_name, version);
416
417                if !is_installed || force {
418                    // TODO: Install the tool using the plugin system
419                    // For now, just record what would be installed
420                    installed_tools.push(format!("{}@{}", tool_name, version));
421                }
422            }
423        }
424
425        Ok(installed_tools)
426    }
427
428    /// Get project tool version from configuration
429    pub fn get_project_tool_version(&self, tool_name: &str) -> Option<String> {
430        // First check if we have project info with tool versions
431        if let Some(project_info) = &self.project_info {
432            if let Some(version) = project_info.tool_versions.get(tool_name) {
433                return Some(version.clone());
434            }
435        }
436
437        // Then check tool-specific configuration
438        if let Some(tool_config) = self.config.tools.get(tool_name) {
439            return tool_config.version.clone();
440        }
441
442        None
443    }
444
445    /// Get configuration status for diagnostics
446    pub fn get_status(&self) -> ConfigStatus {
447        let mut layers = Vec::new();
448
449        // Check which layers are active
450        layers.push(LayerInfo {
451            name: "builtin".to_string(),
452            available: true,
453            priority: 10,
454        });
455
456        if let Some(config_dir) = dirs::config_dir() {
457            let global_config = config_dir.join("vx").join("config.toml");
458            layers.push(LayerInfo {
459                name: "user".to_string(),
460                available: global_config.exists(),
461                priority: 50,
462            });
463        }
464
465        let project_config = PathBuf::from(".vx.toml");
466        layers.push(LayerInfo {
467            name: "project".to_string(),
468            available: project_config.exists(),
469            priority: 80,
470        });
471
472        layers.push(LayerInfo {
473            name: "environment".to_string(),
474            available: std::env::vars().any(|(k, _)| k.starts_with("VX_")),
475            priority: 100,
476        });
477
478        ConfigStatus {
479            layers,
480            available_tools: self.get_available_tools(),
481            fallback_enabled: self.config.defaults.fallback_to_builtin,
482            project_info: self.project_info.clone(),
483        }
484    }
485
486    /// Detect project information and configuration files
487    fn detect_project_info() -> Result<Option<ProjectInfo>> {
488        let current_dir = std::env::current_dir().map_err(|e| VxError::Other {
489            message: format!("Failed to get current directory: {}", e),
490        })?;
491        let mut detected_projects = Vec::new();
492        let mut all_tool_versions = HashMap::new();
493
494        // Check for Python project (pyproject.toml)
495        let pyproject_path = current_dir.join("pyproject.toml");
496        if pyproject_path.exists() {
497            if let Ok(versions) = Self::parse_pyproject_toml(&pyproject_path) {
498                detected_projects.push(ProjectType::Python);
499                all_tool_versions.extend(versions);
500            }
501        }
502
503        // Check for Rust project (Cargo.toml)
504        let cargo_path = current_dir.join("Cargo.toml");
505        if cargo_path.exists() {
506            if let Ok(versions) = Self::parse_cargo_toml(&cargo_path) {
507                detected_projects.push(ProjectType::Rust);
508                all_tool_versions.extend(versions);
509            }
510        }
511
512        // Check for Node.js project (package.json)
513        let package_path = current_dir.join("package.json");
514        if package_path.exists() {
515            if let Ok(versions) = Self::parse_package_json(&package_path) {
516                detected_projects.push(ProjectType::Node);
517                all_tool_versions.extend(versions);
518            }
519        }
520
521        // Check for Go project (go.mod)
522        let gomod_path = current_dir.join("go.mod");
523        if gomod_path.exists() {
524            if let Ok(versions) = Self::parse_go_mod(&gomod_path) {
525                detected_projects.push(ProjectType::Go);
526                all_tool_versions.extend(versions);
527            }
528        }
529
530        if detected_projects.is_empty() {
531            return Ok(None);
532        }
533
534        let project_type = if detected_projects.len() == 1 {
535            detected_projects[0].clone()
536        } else {
537            ProjectType::Mixed
538        };
539
540        // Use the first detected config file as primary
541        let config_file = match project_type {
542            ProjectType::Python => pyproject_path,
543            ProjectType::Rust => cargo_path,
544            ProjectType::Node => package_path,
545            ProjectType::Go => gomod_path,
546            ProjectType::Mixed => {
547                // Prefer pyproject.toml for mixed projects
548                if pyproject_path.exists() {
549                    pyproject_path
550                } else if cargo_path.exists() {
551                    cargo_path
552                } else if package_path.exists() {
553                    package_path
554                } else {
555                    gomod_path
556                }
557            }
558            ProjectType::Unknown => return Ok(None),
559        };
560
561        Ok(Some(ProjectInfo {
562            project_type,
563            config_file,
564            tool_versions: all_tool_versions,
565        }))
566    }
567
568    /// Build the complete figment with all configuration layers
569    fn build_figment(project_info: &Option<ProjectInfo>) -> Result<Figment> {
570        let mut figment = Figment::new();
571
572        // Layer 1: Built-in defaults (lowest priority)
573        figment = figment.merge(Serialized::defaults(VxConfig::default()));
574
575        // Layer 2: Global user configuration
576        if let Some(config_dir) = dirs::config_dir() {
577            let global_config = config_dir.join("vx").join("config.toml");
578            if global_config.exists() {
579                figment = figment.merge(Toml::file(global_config));
580            }
581        }
582
583        // Layer 3: Project-specific tool versions (from project config files)
584        if let Some(project_info) = project_info {
585            let project_config = Self::create_project_config_from_info(project_info)?;
586            figment = figment.merge(Serialized::defaults(project_config));
587        }
588
589        // Layer 4: vx-specific project configuration (.vx.toml)
590        let vx_project_config = PathBuf::from(".vx.toml");
591        if vx_project_config.exists() {
592            // Parse .vx.toml as project config and convert to VxConfig format
593            if let Ok(project_config) = Self::parse_vx_project_config(&vx_project_config) {
594                figment = figment.merge(Serialized::defaults(project_config));
595            }
596        }
597
598        // Layer 5: Environment variables (highest priority)
599        figment = figment.merge(Env::prefixed("VX_"));
600
601        Ok(figment)
602    }
603
604    /// Create project configuration from detected project info
605    fn create_project_config_from_info(project_info: &ProjectInfo) -> Result<VxConfig> {
606        let mut config = VxConfig::default();
607
608        for (tool_name, version) in &project_info.tool_versions {
609            config.tools.insert(
610                tool_name.clone(),
611                ToolConfig {
612                    version: Some(version.clone()),
613                    install_method: None,
614                    registry: None,
615                    custom_sources: None,
616                },
617            );
618        }
619
620        Ok(config)
621    }
622
623    /// Parse pyproject.toml for tool version requirements
624    fn parse_pyproject_toml(path: &PathBuf) -> Result<HashMap<String, String>> {
625        let content = fs::read_to_string(path).map_err(|e| VxError::Other {
626            message: format!("Failed to read pyproject.toml: {}", e),
627        })?;
628        let parsed: toml::Value = toml::from_str(&content).map_err(|e| VxError::Other {
629            message: format!("Failed to parse pyproject.toml: {}", e),
630        })?;
631        let mut versions = HashMap::new();
632
633        // Check for Python version requirement
634        if let Some(project) = parsed.get("project") {
635            if let Some(requires_python) = project.get("requires-python") {
636                if let Some(version_str) = requires_python.as_str() {
637                    // Parse version requirement like ">=3.8" to "3.8"
638                    let version = Self::parse_version_requirement(version_str);
639                    versions.insert("python".to_string(), version);
640                }
641            }
642        }
643
644        // Check for tool.uv configuration
645        if let Some(tool) = parsed.get("tool") {
646            if let Some(uv) = tool.get("uv") {
647                if let Some(version) = uv.get("version") {
648                    if let Some(version_str) = version.as_str() {
649                        versions.insert("uv".to_string(), version_str.to_string());
650                    }
651                }
652            }
653        }
654
655        Ok(versions)
656    }
657
658    /// Parse Cargo.toml for tool version requirements
659    fn parse_cargo_toml(path: &PathBuf) -> Result<HashMap<String, String>> {
660        let content = fs::read_to_string(path).map_err(|e| VxError::Other {
661            message: format!("Failed to read Cargo.toml: {}", e),
662        })?;
663        let parsed: toml::Value = toml::from_str(&content).map_err(|e| VxError::Other {
664            message: format!("Failed to parse Cargo.toml: {}", e),
665        })?;
666        let mut versions = HashMap::new();
667
668        // Check for Rust version requirement
669        if let Some(package) = parsed.get("package") {
670            if let Some(rust_version) = package.get("rust-version") {
671                if let Some(version_str) = rust_version.as_str() {
672                    versions.insert("rust".to_string(), version_str.to_string());
673                }
674            }
675        }
676
677        Ok(versions)
678    }
679
680    /// Parse package.json for tool version requirements
681    fn parse_package_json(path: &PathBuf) -> Result<HashMap<String, String>> {
682        let content = fs::read_to_string(path).map_err(|e| VxError::Other {
683            message: format!("Failed to read package.json: {}", e),
684        })?;
685        let parsed: JsonValue = serde_json::from_str(&content).map_err(|e| VxError::Other {
686            message: format!("Failed to parse package.json: {}", e),
687        })?;
688        let mut versions = HashMap::new();
689
690        // Check for Node.js version requirement in engines
691        if let Some(engines) = parsed.get("engines") {
692            if let Some(node_version) = engines.get("node") {
693                if let Some(version_str) = node_version.as_str() {
694                    let version = Self::parse_version_requirement(version_str);
695                    versions.insert("node".to_string(), version);
696                }
697            }
698            if let Some(npm_version) = engines.get("npm") {
699                if let Some(version_str) = npm_version.as_str() {
700                    let version = Self::parse_version_requirement(version_str);
701                    versions.insert("npm".to_string(), version);
702                }
703            }
704        }
705
706        Ok(versions)
707    }
708
709    /// Parse go.mod for Go version requirement
710    fn parse_go_mod(path: &PathBuf) -> Result<HashMap<String, String>> {
711        let content = fs::read_to_string(path).map_err(|e| VxError::Other {
712            message: format!("Failed to read go.mod: {}", e),
713        })?;
714        let mut versions = HashMap::new();
715
716        // Parse go.mod format: "go 1.21"
717        for line in content.lines() {
718            let line = line.trim();
719            if line.starts_with("go ") {
720                let parts: Vec<&str> = line.split_whitespace().collect();
721                if parts.len() >= 2 {
722                    versions.insert("go".to_string(), parts[1].to_string());
723                }
724                break;
725            }
726        }
727
728        Ok(versions)
729    }
730
731    /// Parse .vx.toml project configuration and convert to VxConfig format
732    fn parse_vx_project_config(path: &PathBuf) -> Result<VxConfig> {
733        let content = fs::read_to_string(path).map_err(|e| VxError::Other {
734            message: format!("Failed to read .vx.toml: {}", e),
735        })?;
736
737        // Parse as project config first
738        let project_config: crate::venv::ProjectConfig =
739            toml::from_str(&content).map_err(|e| VxError::Other {
740                message: format!("Failed to parse .vx.toml: {}", e),
741            })?;
742
743        // Convert to VxConfig format
744        let mut vx_config = VxConfig::default();
745
746        // Convert tools from simple string format to ToolConfig format
747        for (tool_name, version) in project_config.tools {
748            vx_config.tools.insert(
749                tool_name,
750                ToolConfig {
751                    version: Some(version),
752                    install_method: None,
753                    registry: None,
754                    custom_sources: None,
755                },
756            );
757        }
758
759        // Apply project settings to defaults
760        vx_config.defaults.auto_install = project_config.settings.auto_install;
761
762        Ok(vx_config)
763    }
764
765    /// Parse version requirement string to extract version
766    fn parse_version_requirement(requirement: &str) -> String {
767        // Remove common prefixes like >=, ^, ~, etc.
768        let cleaned = requirement
769            .trim_start_matches(">=")
770            .trim_start_matches("^")
771            .trim_start_matches("~")
772            .trim_start_matches("=")
773            .trim_start_matches(">");
774
775        // Take the first version number found
776        if let Some(space_pos) = cleaned.find(' ') {
777            cleaned[..space_pos].to_string()
778        } else {
779            cleaned.to_string()
780        }
781    }
782}