envx_cli/
cli.rs

1use clap::Args;
2use clap::{Parser, Subcommand};
3use color_eyre::Result;
4use color_eyre::eyre::eyre;
5use comfy_table::Attribute;
6use comfy_table::Cell;
7use comfy_table::Color;
8use comfy_table::ContentArrangement;
9use comfy_table::Table;
10use console::Term;
11use console::style;
12use envx_core::PathManager;
13use envx_core::profile_manager::ProfileManager;
14use envx_core::snapshot_manager::SnapshotManager;
15use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
16use std::io::Write;
17use std::path::Path;
18#[derive(Parser)]
19#[command(name = "envx")]
20#[command(about = "System Environment Variable Manager")]
21#[command(version)]
22pub struct Cli {
23    #[command(subcommand)]
24    pub command: Commands,
25}
26
27#[derive(Subcommand)]
28pub enum Commands {
29    /// List environment variables
30    List {
31        /// Filter by source (system, user, process, shell)
32        #[arg(short, long)]
33        source: Option<String>,
34
35        /// Search query
36        #[arg(short = 'q', long)]
37        query: Option<String>,
38
39        /// Output format (json, table, simple, compact)
40        #[arg(short, long, default_value = "table")]
41        format: String,
42
43        /// Sort by (name, value, source)
44        #[arg(long, default_value = "name")]
45        sort: String,
46
47        /// Show only variable names
48        #[arg(long)]
49        names_only: bool,
50
51        /// Limit output to N entries
52        #[arg(short, long)]
53        limit: Option<usize>,
54
55        /// Show statistics summary
56        #[arg(long)]
57        stats: bool,
58    },
59
60    /// Get a specific environment variable
61    Get {
62        /// Variable name or pattern (supports *, ?, and /regex/)
63        /// Examples:
64        ///   envx get PATH           - exact match
65        ///   envx get PATH*          - starts with PATH
66        ///   envx get *PATH          - ends with PATH
67        ///   envx get *PATH*         - contains PATH
68        ///   envx get P?TH           - P followed by any char, then TH
69        ///   envx get /^JAVA.*/      - regex pattern
70        pattern: String,
71
72        /// Output format (simple, detailed, json)
73        #[arg(short, long, default_value = "simple")]
74        format: String,
75    },
76
77    /// Set an environment variable
78    Set {
79        /// Variable name
80        name: String,
81
82        /// Variable value
83        value: String,
84
85        /// Set as temporary (only for current session)
86        #[arg(short, long)]
87        temporary: bool,
88    },
89
90    /// Delete environment variable(s)
91    Delete {
92        /// Variable name or pattern
93        pattern: String,
94
95        /// Force deletion without confirmation
96        #[arg(short, long)]
97        force: bool,
98    },
99
100    /// Analyze environment variables
101    Analyze {
102        /// Type of analysis (duplicates, invalid)
103        #[arg(short, long, default_value = "all")]
104        analysis_type: String,
105    },
106
107    /// Launch the TUI
108    #[command(visible_alias = "ui")]
109    Tui,
110
111    /// Manage PATH variable
112    Path {
113        #[command(subcommand)]
114        action: Option<PathAction>,
115
116        /// Check if all paths exist
117        #[arg(short, long)]
118        check: bool,
119
120        /// Target PATH variable (PATH, Path, or custom like PYTHONPATH)
121        #[arg(short = 'v', long, default_value = "PATH")]
122        var: String,
123
124        /// Apply changes permanently
125        #[arg(short = 'p', long)]
126        permanent: bool,
127    },
128
129    /// Export environment variables to a file
130    Export {
131        /// Output file path
132        file: String,
133
134        /// Variable names or patterns to export (exports all if not specified)
135        #[arg(short = 'v', long)]
136        vars: Vec<String>,
137
138        /// Export format (auto-detect from extension, or: env, json, yaml, txt)
139        #[arg(short, long)]
140        format: Option<String>,
141
142        /// Include only specific sources (system, user, process, shell)
143        #[arg(short, long)]
144        source: Option<String>,
145
146        /// Include metadata (source, modified time)
147        #[arg(short, long)]
148        metadata: bool,
149
150        /// Overwrite existing file without confirmation
151        #[arg(long)]
152        force: bool,
153    },
154
155    /// Import environment variables from a file
156    Import {
157        /// Input file path
158        file: String,
159
160        /// Variable names or patterns to import (imports all if not specified)
161        #[arg(short = 'v', long)]
162        vars: Vec<String>,
163
164        /// Import format (auto-detect from extension, or: env, json, yaml, txt)
165        #[arg(short, long)]
166        format: Option<String>,
167
168        /// Make imported variables permanent
169        #[arg(short, long)]
170        permanent: bool,
171
172        /// Prefix to add to all imported variable names
173        #[arg(long)]
174        prefix: Option<String>,
175
176        /// Overwrite existing variables without confirmation
177        #[arg(long)]
178        overwrite: bool,
179
180        /// Dry run - show what would be imported without making changes
181        #[arg(short = 'n', long)]
182        dry_run: bool,
183    },
184
185    /// Manage environment snapshots
186    Snapshot(SnapshotArgs),
187
188    /// Manage environment profiles
189    Profile(ProfileArgs),
190}
191
192#[derive(Subcommand)]
193pub enum PathAction {
194    /// Add a directory to PATH
195    Add {
196        /// Directory to add
197        directory: String,
198
199        /// Add to the beginning of PATH (highest priority)
200        #[arg(short, long)]
201        first: bool,
202
203        /// Create directory if it doesn't exist
204        #[arg(short, long)]
205        create: bool,
206    },
207
208    /// Remove a directory from PATH
209    Remove {
210        /// Directory to remove (supports wildcards)
211        directory: String,
212
213        /// Remove all occurrences (not just first)
214        #[arg(short, long)]
215        all: bool,
216    },
217
218    /// Clean invalid/non-existent entries from PATH
219    Clean {
220        /// Also remove duplicate entries
221        #[arg(short, long)]
222        dedupe: bool,
223
224        /// Dry run - show what would be removed without making changes
225        #[arg(short = 'n', long)]
226        dry_run: bool,
227    },
228
229    /// Remove duplicate entries from PATH
230    Dedupe {
231        /// Keep first occurrence (default is last)
232        #[arg(short, long)]
233        keep_first: bool,
234
235        /// Dry run - show what would be removed
236        #[arg(short = 'n', long)]
237        dry_run: bool,
238    },
239
240    /// Check PATH for issues
241    Check {
242        /// Verbose output
243        #[arg(short, long)]
244        verbose: bool,
245    },
246
247    /// Show PATH entries in order
248    List {
249        /// Show with index numbers
250        #[arg(short, long)]
251        numbered: bool,
252
253        /// Check existence of each path
254        #[arg(short, long)]
255        check: bool,
256    },
257
258    /// Move a PATH entry to a different position
259    Move {
260        /// Path or index to move
261        from: String,
262
263        /// Target position (first, last, or index)
264        to: String,
265    },
266}
267
268#[derive(Args)]
269pub struct SnapshotArgs {
270    #[command(subcommand)]
271    pub command: SnapshotCommands,
272}
273
274#[derive(Subcommand)]
275pub enum SnapshotCommands {
276    /// Create a new snapshot
277    Create {
278        /// Snapshot name
279        name: String,
280        /// Description
281        #[arg(short, long)]
282        description: Option<String>,
283    },
284    /// List all snapshots
285    List,
286    /// Show details of a snapshot
287    Show {
288        /// Snapshot name or ID
289        snapshot: String,
290    },
291    /// Restore from a snapshot
292    Restore {
293        /// Snapshot name or ID
294        snapshot: String,
295        /// Force restore without confirmation
296        #[arg(short, long)]
297        force: bool,
298    },
299    /// Delete a snapshot
300    Delete {
301        /// Snapshot name or ID
302        snapshot: String,
303        /// Force deletion without confirmation
304        #[arg(short, long)]
305        force: bool,
306    },
307    /// Compare two snapshots
308    Diff {
309        /// First snapshot
310        snapshot1: String,
311        /// Second snapshot
312        snapshot2: String,
313    },
314}
315
316#[derive(Args)]
317pub struct ProfileArgs {
318    #[command(subcommand)]
319    pub command: ProfileCommands,
320}
321
322#[derive(Subcommand)]
323pub enum ProfileCommands {
324    /// Create a new profile
325    Create {
326        /// Profile name
327        name: String,
328        /// Description
329        #[arg(short, long)]
330        description: Option<String>,
331    },
332    /// List all profiles
333    List,
334    /// Show current or specific profile
335    Show {
336        /// Profile name (shows active if not specified)
337        name: Option<String>,
338    },
339    /// Switch to a profile
340    Switch {
341        /// Profile name
342        name: String,
343        /// Apply immediately
344        #[arg(short, long)]
345        apply: bool,
346    },
347    /// Add a variable to a profile
348    Add {
349        /// Profile name
350        profile: String,
351        /// Variable name
352        name: String,
353        /// Variable value
354        value: String,
355        /// Override system variable
356        #[arg(short, long)]
357        override_system: bool,
358    },
359    /// Remove a variable from a profile
360    Remove {
361        /// Profile name
362        profile: String,
363        /// Variable name
364        name: String,
365    },
366    /// Delete a profile
367    Delete {
368        /// Profile name
369        name: String,
370        /// Force deletion
371        #[arg(short, long)]
372        force: bool,
373    },
374    /// Export a profile
375    Export {
376        /// Profile name
377        name: String,
378        /// Output file (stdout if not specified)
379        #[arg(short, long)]
380        output: Option<String>,
381    },
382    /// Import a profile
383    Import {
384        /// Import file
385        file: String,
386        /// Profile name
387        #[arg(short, long)]
388        name: Option<String>,
389        /// Overwrite if exists
390        #[arg(short, long)]
391        overwrite: bool,
392    },
393    /// Apply a profile to current environment
394    Apply {
395        /// Profile name
396        name: String,
397    },
398}
399
400/// Execute the CLI command with the given arguments.
401///
402/// # Errors
403///
404/// This function will return an error if:
405/// - Environment variable operations fail (loading, setting, deleting)
406/// - File I/O operations fail (import/export)
407/// - User input cannot be read
408/// - Invalid command arguments are provided
409/// - TUI mode is requested (should be handled by main binary)
410pub fn execute(cli: Cli) -> Result<()> {
411    match cli.command {
412        Commands::List {
413            source,
414            query,
415            format,
416            sort,
417            names_only,
418            limit,
419            stats,
420        } => {
421            handle_list_command(
422                source.as_deref(),
423                query.as_deref(),
424                &format,
425                &sort,
426                names_only,
427                limit,
428                stats,
429            )?;
430        }
431
432        Commands::Get { pattern, format } => {
433            handle_get_command(&pattern, &format)?;
434        }
435
436        Commands::Set { name, value, temporary } => {
437            handle_set_command(&name, &value, temporary)?;
438        }
439
440        Commands::Delete { pattern, force } => {
441            handle_delete_command(&pattern, force)?;
442        }
443
444        Commands::Analyze { analysis_type } => {
445            handle_analyze_command(&analysis_type)?;
446        }
447
448        Commands::Tui => {
449            // Launch the TUI
450            envx_tui::run()?;
451        }
452
453        Commands::Path {
454            action,
455            check,
456            var,
457            permanent,
458        } => {
459            handle_path_command(action, check, &var, permanent)?;
460        }
461
462        Commands::Export {
463            file,
464            vars,
465            format,
466            source,
467            metadata,
468            force,
469        } => {
470            handle_export(&file, &vars, format, source, metadata, force)?;
471        }
472
473        Commands::Import {
474            file,
475            vars,
476            format,
477            permanent,
478            prefix,
479            overwrite,
480            dry_run,
481        } => {
482            handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
483        }
484
485        Commands::Snapshot(args) => {
486            handle_snapshot(args)?;
487        }
488        Commands::Profile(args) => {
489            handle_profile(args)?;
490        }
491    }
492
493    Ok(())
494}
495
496fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
497    let mut manager = EnvVarManager::new();
498    manager.load_all()?;
499
500    let vars = manager.get_pattern(pattern);
501
502    if vars.is_empty() {
503        eprintln!("No variables found matching pattern: {pattern}");
504        return Ok(());
505    }
506
507    match format {
508        "json" => {
509            println!("{}", serde_json::to_string_pretty(&vars)?);
510        }
511        "detailed" => {
512            for var in vars {
513                println!("Name: {}", var.name);
514                println!("Value: {}", var.value);
515                println!("Source: {:?}", var.source);
516                println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
517                if let Some(orig) = &var.original_value {
518                    println!("Original: {orig}");
519                }
520                println!("---");
521            }
522        }
523        _ => {
524            for var in vars {
525                println!("{} = {}", var.name, var.value);
526            }
527        }
528    }
529    Ok(())
530}
531
532fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
533    let mut manager = EnvVarManager::new();
534    manager.load_all()?;
535
536    let permanent = !temporary;
537
538    manager.set(name, value, permanent)?;
539    if permanent {
540        println!("āœ… Set {name} = \"{value}\"");
541        #[cfg(windows)]
542        println!("šŸ“ Note: You may need to restart your terminal for changes to take effect");
543    } else {
544        println!("⚔ Set {name} = \"{value}\" (temporary - current session only)");
545    }
546    Ok(())
547}
548
549fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
550    let mut manager = EnvVarManager::new();
551    manager.load_all()?;
552
553    // Collect the names to delete first (owned data, not references)
554    let vars_to_delete: Vec<String> = manager
555        .get_pattern(pattern)
556        .into_iter()
557        .map(|v| v.name.clone())
558        .collect();
559
560    if vars_to_delete.is_empty() {
561        eprintln!("No variables found matching pattern: {pattern}");
562        return Ok(());
563    }
564
565    if !force && vars_to_delete.len() > 1 {
566        println!("About to delete {} variables:", vars_to_delete.len());
567        for name in &vars_to_delete {
568            println!("  - {name}");
569        }
570        print!("Continue? [y/N]: ");
571        std::io::stdout().flush()?;
572
573        let mut input = String::new();
574        std::io::stdin().read_line(&mut input)?;
575
576        if !input.trim().eq_ignore_ascii_case("y") {
577            println!("Cancelled.");
578            return Ok(());
579        }
580    }
581
582    // Now we can safely delete since we're not holding any references to manager
583    for name in vars_to_delete {
584        manager.delete(&name)?;
585        println!("Deleted: {name}");
586    }
587    Ok(())
588}
589
590fn handle_analyze_command(analysis_type: &str) -> Result<()> {
591    let mut manager = EnvVarManager::new();
592    manager.load_all()?;
593    let vars = manager.list().into_iter().cloned().collect();
594    let analyzer = Analyzer::new(vars);
595
596    match analysis_type {
597        "duplicates" | "all" => {
598            let duplicates = analyzer.find_duplicates();
599            if !duplicates.is_empty() {
600                println!("Duplicate variables found:");
601                for (name, vars) in duplicates {
602                    println!("  {}: {} instances", name, vars.len());
603                }
604            }
605        }
606        "invalid" => {
607            let validation = analyzer.validate_all();
608            for (name, result) in validation {
609                if !result.valid {
610                    println!("Invalid variable: {name}");
611                    for error in result.errors {
612                        println!("  Error: {error}");
613                    }
614                }
615            }
616        }
617        _ => {}
618    }
619    Ok(())
620}
621
622#[allow(clippy::too_many_lines)]
623fn handle_path_command(action: Option<PathAction>, check: bool, var: &str, permanent: bool) -> Result<()> {
624    let mut manager = EnvVarManager::new();
625    manager.load_all()?;
626
627    // Get the PATH variable
628    let path_var = manager.get(var).ok_or_else(|| eyre!("Variable '{}' not found", var))?;
629
630    let mut path_mgr = PathManager::new(&path_var.value);
631
632    // If no action specified, list PATH entries
633    if action.is_none() {
634        if check {
635            handle_path_check(&path_mgr, true);
636        }
637        handle_path_list(&path_mgr, false, false);
638    }
639
640    let command = action.expect("Action should be Some if we reach here");
641    match command {
642        PathAction::Add {
643            directory,
644            first,
645            create,
646        } => {
647            let path = Path::new(&directory);
648
649            // Check if directory exists
650            if !path.exists() {
651                if create {
652                    std::fs::create_dir_all(path)?;
653                    println!("Created directory: {directory}");
654                } else if !path.exists() {
655                    eprintln!("Warning: Directory does not exist: {directory}");
656                    print!("Add anyway? [y/N]: ");
657                    std::io::stdout().flush()?;
658
659                    let mut input = String::new();
660                    std::io::stdin().read_line(&mut input)?;
661
662                    if !input.trim().eq_ignore_ascii_case("y") {
663                        return Ok(());
664                    }
665                }
666            }
667
668            // Check if already in PATH
669            if path_mgr.contains(&directory) {
670                println!("Directory already in {var}: {directory}");
671                return Ok(());
672            }
673
674            // Add to PATH
675            if first {
676                path_mgr.add_first(directory.clone());
677                println!("Added to beginning of {var}: {directory}");
678            } else {
679                path_mgr.add_last(directory.clone());
680                println!("Added to end of {var}: {directory}");
681            }
682
683            // Save changes
684            let new_value = path_mgr.to_string();
685            manager.set(var, &new_value, permanent)?;
686        }
687
688        PathAction::Remove { directory, all } => {
689            let removed = if all {
690                path_mgr.remove_all(&directory)
691            } else {
692                path_mgr.remove_first(&directory)
693            };
694
695            if removed > 0 {
696                println!("Removed {removed} occurrence(s) of: {directory}");
697                let new_value = path_mgr.to_string();
698                manager.set(var, &new_value, permanent)?;
699            } else {
700                println!("Directory not found in {var}: {directory}");
701            }
702        }
703
704        PathAction::Clean { dedupe, dry_run } => {
705            let invalid = path_mgr.get_invalid();
706            let duplicates = if dedupe { path_mgr.get_duplicates() } else { vec![] };
707
708            if invalid.is_empty() && duplicates.is_empty() {
709                println!("No invalid or duplicate entries found in {var}");
710                return Ok(());
711            }
712
713            if !invalid.is_empty() {
714                println!("Invalid/non-existent paths to remove:");
715                for path in &invalid {
716                    println!("  - {path}");
717                }
718            }
719
720            if !duplicates.is_empty() {
721                println!("Duplicate paths to remove:");
722                for path in &duplicates {
723                    println!("  - {path}");
724                }
725            }
726
727            if dry_run {
728                println!("\n(Dry run - no changes made)");
729            } else {
730                let removed_invalid = path_mgr.remove_invalid();
731                let removed_dupes = if dedupe {
732                    path_mgr.deduplicate(false) // Keep last by default
733                } else {
734                    0
735                };
736
737                println!("Removed {removed_invalid} invalid and {removed_dupes} duplicate entries");
738                let new_value = path_mgr.to_string();
739                manager.set(var, &new_value, permanent)?;
740            }
741        }
742
743        PathAction::Dedupe { keep_first, dry_run } => {
744            let duplicates = path_mgr.get_duplicates();
745
746            if duplicates.is_empty() {
747                println!("No duplicate entries found in {var}");
748                return Ok(());
749            }
750
751            println!("Duplicate paths to remove:");
752            for path in &duplicates {
753                println!("  - {path}");
754            }
755            println!(
756                "Strategy: keep {} occurrence",
757                if keep_first { "first" } else { "last" }
758            );
759
760            if dry_run {
761                println!("\n(Dry run - no changes made)");
762            } else {
763                let removed = path_mgr.deduplicate(keep_first);
764                println!("Removed {removed} duplicate entries");
765                let new_value = path_mgr.to_string();
766                manager.set(var, &new_value, permanent)?;
767            }
768        }
769
770        PathAction::Check { verbose } => {
771            handle_path_check(&path_mgr, verbose);
772        }
773
774        PathAction::List { numbered, check } => {
775            handle_path_list(&path_mgr, numbered, check);
776        }
777
778        PathAction::Move { from, to } => {
779            // Parse from (can be index or path)
780            let from_idx = if let Ok(idx) = from.parse::<usize>() {
781                idx
782            } else {
783                path_mgr
784                    .find_index(&from)
785                    .ok_or_else(|| eyre!("Path not found: {}", from))?
786            };
787
788            // Parse to (can be "first", "last", or index)
789            let to_idx = match to.as_str() {
790                "first" => 0,
791                "last" => path_mgr.len() - 1,
792                _ => to.parse::<usize>().map_err(|_| eyre!("Invalid position: {}", to))?,
793            };
794
795            path_mgr.move_entry(from_idx, to_idx)?;
796            println!("Moved entry from position {from_idx} to {to_idx}");
797
798            let new_value = path_mgr.to_string();
799            manager.set(var, &new_value, permanent)?;
800        }
801    }
802
803    Ok(())
804}
805
806fn handle_path_check(path_mgr: &PathManager, verbose: bool) {
807    let entries = path_mgr.entries();
808    let mut issues = Vec::new();
809    let mut valid_count = 0;
810
811    for (idx, entry) in entries.iter().enumerate() {
812        let path = Path::new(entry);
813        let exists = path.exists();
814        let is_dir = path.is_dir();
815
816        if verbose || !exists {
817            let status = if !exists {
818                issues.push(format!("Not found: {entry}"));
819                "āŒ NOT FOUND"
820            } else if !is_dir {
821                issues.push(format!("Not a directory: {entry}"));
822                "āš ļø  NOT DIR"
823            } else {
824                valid_count += 1;
825                "āœ“ OK"
826            };
827
828            if verbose {
829                println!("[{idx:3}] {status} - {entry}");
830            }
831        } else if exists && is_dir {
832            valid_count += 1;
833        }
834    }
835
836    // Summary
837    println!("\nPATH Analysis:");
838    println!("  Total entries: {}", entries.len());
839    println!("  Valid entries: {valid_count}");
840
841    let duplicates = path_mgr.get_duplicates();
842    if !duplicates.is_empty() {
843        println!("  Duplicates: {} entries", duplicates.len());
844        if verbose {
845            for dup in &duplicates {
846                println!("    - {dup}");
847            }
848        }
849    }
850
851    let invalid = path_mgr.get_invalid();
852    if !invalid.is_empty() {
853        println!("  Invalid entries: {}", invalid.len());
854        if verbose {
855            for inv in &invalid {
856                println!("    - {inv}");
857            }
858        }
859    }
860
861    if issues.is_empty() {
862        println!("\nāœ… No issues found!");
863    } else {
864        println!("\nāš ļø  {} issue(s) found", issues.len());
865        if !verbose {
866            println!("Run with --verbose for details");
867        }
868    }
869}
870
871fn handle_path_list(path_mgr: &PathManager, numbered: bool, check: bool) {
872    let entries = path_mgr.entries();
873
874    if entries.is_empty() {
875        println!("PATH is empty");
876    }
877
878    for (idx, entry) in entries.iter().enumerate() {
879        let prefix = if numbered { format!("[{idx:3}] ") } else { String::new() };
880
881        let suffix = if check {
882            let path = Path::new(entry);
883            if !path.exists() {
884                " [NOT FOUND]"
885            } else if !path.is_dir() {
886                " [NOT A DIRECTORY]"
887            } else {
888                ""
889            }
890        } else {
891            ""
892        };
893
894        println!("{prefix}{entry}{suffix}");
895    }
896}
897
898fn handle_export(
899    file: &str,
900    vars: &[String],
901    format: Option<String>,
902    source: Option<String>,
903    metadata: bool,
904    force: bool,
905) -> Result<()> {
906    // Check if file exists
907    if Path::new(&file).exists() && !force {
908        print!("File '{file}' already exists. Overwrite? [y/N]: ");
909        std::io::stdout().flush()?;
910
911        let mut input = String::new();
912        std::io::stdin().read_line(&mut input)?;
913
914        if !input.trim().eq_ignore_ascii_case("y") {
915            println!("Export cancelled.");
916            return Ok(());
917        }
918    }
919
920    // Load environment variables
921    let mut manager = EnvVarManager::new();
922    manager.load_all()?;
923
924    // Filter variables to export
925    let mut vars_to_export = if vars.is_empty() {
926        manager.list().into_iter().cloned().collect()
927    } else {
928        let mut selected = Vec::new();
929        for pattern in vars {
930            let matched = manager.get_pattern(pattern);
931            selected.extend(matched.into_iter().cloned());
932        }
933        selected
934    };
935
936    // Filter by source if specified
937    if let Some(src) = source {
938        let source_filter = match src.as_str() {
939            "system" => envx_core::EnvVarSource::System,
940            "user" => envx_core::EnvVarSource::User,
941            "process" => envx_core::EnvVarSource::Process,
942            "shell" => envx_core::EnvVarSource::Shell,
943            _ => return Err(eyre!("Invalid source: {}", src)),
944        };
945
946        vars_to_export.retain(|v| v.source == source_filter);
947    }
948
949    if vars_to_export.is_empty() {
950        println!("No variables to export.");
951        return Ok(());
952    }
953
954    // Determine format
955    let export_format = if let Some(fmt) = format {
956        match fmt.as_str() {
957            "env" => ExportFormat::DotEnv,
958            "json" => ExportFormat::Json,
959            "yaml" | "yml" => ExportFormat::Yaml,
960            "txt" | "text" => ExportFormat::Text,
961            "ps1" | "powershell" => ExportFormat::PowerShell,
962            "sh" | "bash" => ExportFormat::Shell,
963            _ => return Err(eyre!("Unsupported format: {}", fmt)),
964        }
965    } else {
966        // Auto-detect from extension
967        ExportFormat::from_extension(file)?
968    };
969
970    // Export
971    let exporter = Exporter::new(vars_to_export, metadata);
972    exporter.export_to_file(file, export_format)?;
973
974    println!("Exported {} variables to '{}'", exporter.count(), file);
975
976    Ok(())
977}
978
979fn handle_import(
980    file: &str,
981    vars: &[String],
982    format: Option<String>,
983    permanent: bool,
984    prefix: Option<&String>,
985    overwrite: bool,
986    dry_run: bool,
987) -> Result<()> {
988    // Check if file exists
989    if !Path::new(&file).exists() {
990        return Err(eyre!("File not found: {}", file));
991    }
992
993    // Determine format
994    let import_format = if let Some(fmt) = format {
995        match fmt.as_str() {
996            "env" => ImportFormat::DotEnv,
997            "json" => ImportFormat::Json,
998            "yaml" | "yml" => ImportFormat::Yaml,
999            "txt" | "text" => ImportFormat::Text,
1000            _ => return Err(eyre!("Unsupported format: {}", fmt)),
1001        }
1002    } else {
1003        // Auto-detect from extension
1004        ImportFormat::from_extension(file)?
1005    };
1006
1007    // Import variables
1008    let mut importer = Importer::new();
1009    importer.import_from_file(file, import_format)?;
1010
1011    // Filter variables if patterns specified
1012    if !vars.is_empty() {
1013        importer.filter_by_patterns(vars);
1014    }
1015
1016    // Add prefix if specified
1017    if let Some(pfx) = &prefix {
1018        importer.add_prefix(pfx);
1019    }
1020
1021    // Get variables to import
1022    let import_vars = importer.get_variables();
1023
1024    if import_vars.is_empty() {
1025        println!("No variables to import.");
1026        return Ok(());
1027    }
1028
1029    // Check for conflicts
1030    let mut manager = EnvVarManager::new();
1031    manager.load_all()?;
1032
1033    let mut conflicts = Vec::new();
1034    for (name, _) in &import_vars {
1035        if manager.get(name).is_some() {
1036            conflicts.push(name.clone());
1037        }
1038    }
1039
1040    if !conflicts.is_empty() && !overwrite && !dry_run {
1041        println!("The following variables already exist:");
1042        for name in &conflicts {
1043            println!("  - {name}");
1044        }
1045
1046        print!("Overwrite existing variables? [y/N]: ");
1047        std::io::stdout().flush()?;
1048
1049        let mut input = String::new();
1050        std::io::stdin().read_line(&mut input)?;
1051
1052        if !input.trim().eq_ignore_ascii_case("y") {
1053            println!("Import cancelled.");
1054            return Ok(());
1055        }
1056    }
1057
1058    // Preview or apply changes
1059    if dry_run {
1060        println!("Would import {} variables:", import_vars.len());
1061        for (name, value) in &import_vars {
1062            let status = if conflicts.contains(name) {
1063                " [OVERWRITE]"
1064            } else {
1065                " [NEW]"
1066            };
1067            println!(
1068                "  {} = {}{}",
1069                name,
1070                if value.len() > 50 {
1071                    format!("{}...", &value[..50])
1072                } else {
1073                    value.clone()
1074                },
1075                status
1076            );
1077        }
1078        println!("\n(Dry run - no changes made)");
1079    } else {
1080        // Apply imports
1081        let mut imported = 0;
1082        let mut failed = 0;
1083
1084        for (name, value) in import_vars {
1085            match manager.set(&name, &value, permanent) {
1086                Ok(()) => imported += 1,
1087                Err(e) => {
1088                    eprintln!("Failed to import {name}: {e}");
1089                    failed += 1;
1090                }
1091            }
1092        }
1093
1094        println!("Imported {imported} variables");
1095        if failed > 0 {
1096            println!("Failed to import {failed} variables");
1097        }
1098    }
1099
1100    Ok(())
1101}
1102
1103fn handle_list_command(
1104    source: Option<&str>,
1105    query: Option<&str>,
1106    format: &str,
1107    sort: &str,
1108    names_only: bool,
1109    limit: Option<usize>,
1110    stats: bool,
1111) -> Result<()> {
1112    let mut manager = EnvVarManager::new();
1113    manager.load_all()?;
1114
1115    // Get filtered variables
1116    let mut vars = if let Some(q) = &query {
1117        manager.search(q)
1118    } else if let Some(src) = source {
1119        let source_filter = match src {
1120            "system" => envx_core::EnvVarSource::System,
1121            "user" => envx_core::EnvVarSource::User,
1122            "process" => envx_core::EnvVarSource::Process,
1123            "shell" => envx_core::EnvVarSource::Shell,
1124            _ => return Err(eyre!("Invalid source: {}", src)),
1125        };
1126        manager.filter_by_source(&source_filter)
1127    } else {
1128        manager.list()
1129    };
1130
1131    // Sort variables
1132    match sort {
1133        "name" => vars.sort_by(|a, b| a.name.cmp(&b.name)),
1134        "value" => vars.sort_by(|a, b| a.value.cmp(&b.value)),
1135        "source" => vars.sort_by(|a, b| format!("{:?}", a.source).cmp(&format!("{:?}", b.source))),
1136        _ => {}
1137    }
1138
1139    // Apply limit if specified
1140    let total_count = vars.len();
1141    if let Some(lim) = limit {
1142        vars.truncate(lim);
1143    }
1144
1145    // Show statistics if requested
1146    if stats || (format == "table" && !names_only) {
1147        print_statistics(&manager, &vars, total_count, query, source);
1148    }
1149
1150    // Handle names_only flag
1151    if names_only {
1152        for var in vars {
1153            println!("{}", var.name);
1154        }
1155        return Ok(());
1156    }
1157
1158    // Format output
1159    match format {
1160        "json" => {
1161            println!("{}", serde_json::to_string_pretty(&vars)?);
1162        }
1163        "simple" => {
1164            for var in vars {
1165                println!("{} = {}", style(&var.name).cyan(), var.value);
1166            }
1167        }
1168        "compact" => {
1169            for var in vars {
1170                let source_str = format_source_compact(&var.source);
1171                println!(
1172                    "{} {} = {}",
1173                    source_str,
1174                    style(&var.name).bright(),
1175                    style(truncate_value(&var.value, 60)).dim()
1176                );
1177            }
1178        }
1179        _ => {
1180            print_table(vars, limit.is_some());
1181        }
1182    }
1183
1184    // Show limit notice
1185    if let Some(lim) = limit {
1186        if total_count > lim {
1187            println!(
1188                "\n{}",
1189                style(format!(
1190                    "Showing {lim} of {total_count} total variables. Use --limit to see more."
1191                ))
1192                .yellow()
1193            );
1194        }
1195    }
1196
1197    Ok(())
1198}
1199
1200/// Handle snapshot-related commands.
1201///
1202/// # Errors
1203///
1204/// This function will return an error if:
1205/// - The snapshot manager cannot be initialized
1206/// - Environment variable loading fails
1207/// - Snapshot operations fail (create, restore, delete, etc.)
1208/// - File I/O operations fail during snapshot operations
1209/// - User input cannot be read from stdin
1210/// - Invalid snapshot names or IDs are provided
1211pub fn handle_snapshot(args: SnapshotArgs) -> Result<()> {
1212    let snapshot_manager = SnapshotManager::new()?;
1213    let mut env_manager = EnvVarManager::new();
1214    env_manager.load_all()?;
1215
1216    match args.command {
1217        SnapshotCommands::Create { name, description } => {
1218            let vars = env_manager.list().into_iter().cloned().collect();
1219            let snapshot = snapshot_manager.create(name, description, vars)?;
1220            println!("āœ… Created snapshot: {} (ID: {})", snapshot.name, snapshot.id);
1221        }
1222        SnapshotCommands::List => {
1223            let snapshots = snapshot_manager.list()?;
1224            if snapshots.is_empty() {
1225                println!("No snapshots found.");
1226                return Ok(());
1227            }
1228
1229            let mut table = Table::new();
1230            table.set_header(vec!["Name", "ID", "Created", "Variables", "Description"]);
1231
1232            for snapshot in snapshots {
1233                table.add_row(vec![
1234                    snapshot.name,
1235                    snapshot.id[..8].to_string(),
1236                    snapshot.created_at.format("%Y-%m-%d %H:%M").to_string(),
1237                    snapshot.variables.len().to_string(),
1238                    snapshot.description.unwrap_or_default(),
1239                ]);
1240            }
1241
1242            println!("{table}");
1243        }
1244        SnapshotCommands::Show { snapshot } => {
1245            let snap = snapshot_manager.get(&snapshot)?;
1246            println!("Snapshot: {}", snap.name);
1247            println!("ID: {}", snap.id);
1248            println!("Created: {}", snap.created_at.format("%Y-%m-%d %H:%M:%S"));
1249            println!("Description: {}", snap.description.unwrap_or_default());
1250            println!("Variables: {}", snap.variables.len());
1251
1252            // Show first 10 variables
1253            println!("\nFirst 10 variables:");
1254            for (i, (name, var)) in snap.variables.iter().take(10).enumerate() {
1255                println!("  {}. {} = {}", i + 1, name, var.value);
1256            }
1257
1258            if snap.variables.len() > 10 {
1259                println!("  ... and {} more", snap.variables.len() - 10);
1260            }
1261        }
1262        SnapshotCommands::Restore { snapshot, force } => {
1263            if !force {
1264                print!("āš ļø  This will replace all current environment variables. Continue? [y/N] ");
1265                std::io::Write::flush(&mut std::io::stdout())?;
1266
1267                let mut input = String::new();
1268                std::io::stdin().read_line(&mut input)?;
1269                if !input.trim().eq_ignore_ascii_case("y") {
1270                    println!("Cancelled.");
1271                    return Ok(());
1272                }
1273            }
1274
1275            snapshot_manager.restore(&snapshot, &mut env_manager)?;
1276            println!("āœ… Restored from snapshot: {snapshot}");
1277        }
1278        SnapshotCommands::Delete { snapshot, force } => {
1279            if !force {
1280                print!("āš ļø  Delete snapshot '{snapshot}'? [y/N] ");
1281                std::io::Write::flush(&mut std::io::stdout())?;
1282
1283                let mut input = String::new();
1284                std::io::stdin().read_line(&mut input)?;
1285                if !input.trim().eq_ignore_ascii_case("y") {
1286                    println!("Cancelled.");
1287                    return Ok(());
1288                }
1289            }
1290
1291            snapshot_manager.delete(&snapshot)?;
1292            println!("āœ… Deleted snapshot: {snapshot}");
1293        }
1294        SnapshotCommands::Diff { snapshot1, snapshot2 } => {
1295            let diff = snapshot_manager.diff(&snapshot1, &snapshot2)?;
1296
1297            if diff.added.is_empty() && diff.removed.is_empty() && diff.modified.is_empty() {
1298                println!("No differences found between snapshots.");
1299                return Ok(());
1300            }
1301
1302            if !diff.added.is_empty() {
1303                println!("āž• Added in {snapshot2}:");
1304                for (name, var) in &diff.added {
1305                    println!("   {} = {}", name, var.value);
1306                }
1307            }
1308
1309            if !diff.removed.is_empty() {
1310                println!("\nāž– Removed in {snapshot2}:");
1311                for (name, var) in &diff.removed {
1312                    println!("   {} = {}", name, var.value);
1313                }
1314            }
1315
1316            if !diff.modified.is_empty() {
1317                println!("\nšŸ”„ Modified:");
1318                for (name, (old, new)) in &diff.modified {
1319                    println!("   {name}:");
1320                    println!("     Old: {}", old.value);
1321                    println!("     New: {}", new.value);
1322                }
1323            }
1324        }
1325    }
1326
1327    Ok(())
1328}
1329
1330/// Handle profile-related commands.
1331///
1332/// # Errors
1333///
1334/// This function will return an error if:
1335/// - The profile manager cannot be initialized
1336/// - Environment variable loading fails  
1337/// - Profile operations fail (create, switch, delete, etc.)
1338/// - File I/O operations fail during profile import/export
1339/// - User input cannot be read from stdin
1340/// - Invalid profile names are provided
1341/// - Profile data cannot be serialized/deserialized
1342pub fn handle_profile(args: ProfileArgs) -> Result<()> {
1343    let mut profile_manager = ProfileManager::new()?;
1344    let mut env_manager = EnvVarManager::new();
1345    env_manager.load_all()?;
1346
1347    match args.command {
1348        ProfileCommands::Create { name, description } => {
1349            handle_profile_create(&mut profile_manager, &name, description)?;
1350        }
1351        ProfileCommands::List => {
1352            handle_profile_list(&profile_manager);
1353        }
1354        ProfileCommands::Show { name } => {
1355            handle_profile_show(&profile_manager, name)?;
1356        }
1357        ProfileCommands::Switch { name, apply } => {
1358            handle_profile_switch(&mut profile_manager, &mut env_manager, &name, apply)?;
1359        }
1360        ProfileCommands::Add {
1361            profile,
1362            name,
1363            value,
1364            override_system,
1365        } => {
1366            handle_profile_add(&mut profile_manager, &profile, &name, &value, override_system)?;
1367        }
1368        ProfileCommands::Remove { profile, name } => {
1369            handle_profile_remove(&mut profile_manager, &profile, &name)?;
1370        }
1371        ProfileCommands::Delete { name, force } => {
1372            handle_profile_delete(&mut profile_manager, &name, force)?;
1373        }
1374        ProfileCommands::Export { name, output } => {
1375            handle_profile_export(&profile_manager, &name, output)?;
1376        }
1377        ProfileCommands::Import { file, name, overwrite } => {
1378            handle_profile_import(&mut profile_manager, &file, name, overwrite)?;
1379        }
1380        ProfileCommands::Apply { name } => {
1381            handle_profile_apply(&mut profile_manager, &mut env_manager, &name)?;
1382        }
1383    }
1384
1385    Ok(())
1386}
1387
1388fn handle_profile_create(profile_manager: &mut ProfileManager, name: &str, description: Option<String>) -> Result<()> {
1389    profile_manager.create(name.to_string(), description)?;
1390    println!("āœ… Created profile: {name}");
1391    Ok(())
1392}
1393
1394fn handle_profile_list(profile_manager: &ProfileManager) {
1395    let profiles = profile_manager.list();
1396    if profiles.is_empty() {
1397        println!("No profiles found.");
1398    }
1399
1400    let active = profile_manager.active().map(|p| &p.name);
1401    let mut table = Table::new();
1402    table.set_header(vec!["Name", "Variables", "Created", "Description", "Status"]);
1403
1404    for profile in profiles {
1405        let status = if active == Some(&profile.name) {
1406            "ā— Active"
1407        } else {
1408            ""
1409        };
1410
1411        table.add_row(vec![
1412            profile.name.clone(),
1413            profile.variables.len().to_string(),
1414            profile.created_at.format("%Y-%m-%d").to_string(),
1415            profile.description.clone().unwrap_or_default(),
1416            status.to_string(),
1417        ]);
1418    }
1419
1420    println!("{table}");
1421}
1422
1423fn handle_profile_show(profile_manager: &ProfileManager, name: Option<String>) -> Result<()> {
1424    let profile = if let Some(name) = name {
1425        profile_manager
1426            .get(&name)
1427            .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", name))?
1428    } else {
1429        profile_manager
1430            .active()
1431            .ok_or_else(|| color_eyre::eyre::eyre!("No active profile"))?
1432    };
1433
1434    println!("Profile: {}", profile.name);
1435    println!("Description: {}", profile.description.as_deref().unwrap_or(""));
1436    println!("Created: {}", profile.created_at.format("%Y-%m-%d %H:%M:%S"));
1437    println!("Updated: {}", profile.updated_at.format("%Y-%m-%d %H:%M:%S"));
1438    if let Some(parent) = &profile.parent {
1439        println!("Inherits from: {parent}");
1440    }
1441    println!("\nVariables:");
1442
1443    for (name, var) in &profile.variables {
1444        let status = if var.enabled { "āœ“" } else { "āœ—" };
1445        let override_flag = if var.override_system { " [override]" } else { "" };
1446        println!("  {} {} = {}{}", status, name, var.value, override_flag);
1447    }
1448    Ok(())
1449}
1450
1451fn handle_profile_switch(
1452    profile_manager: &mut ProfileManager,
1453    env_manager: &mut EnvVarManager,
1454    name: &str,
1455    apply: bool,
1456) -> Result<()> {
1457    profile_manager.switch(name)?;
1458    println!("āœ… Switched to profile: {name}");
1459
1460    if apply {
1461        profile_manager.apply(name, env_manager)?;
1462        println!("āœ… Applied profile variables");
1463    }
1464    Ok(())
1465}
1466
1467fn handle_profile_add(
1468    profile_manager: &mut ProfileManager,
1469    profile: &str,
1470    name: &str,
1471    value: &str,
1472    override_system: bool,
1473) -> Result<()> {
1474    let prof = profile_manager
1475        .get_mut(profile)
1476        .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1477
1478    prof.add_var(name.to_string(), value.to_string(), override_system);
1479    profile_manager.save()?;
1480
1481    println!("āœ… Added {name} to profile {profile}");
1482    Ok(())
1483}
1484
1485fn handle_profile_remove(profile_manager: &mut ProfileManager, profile: &str, name: &str) -> Result<()> {
1486    let prof = profile_manager
1487        .get_mut(profile)
1488        .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1489
1490    prof.remove_var(name)
1491        .ok_or_else(|| color_eyre::eyre::eyre!("Variable '{}' not found in profile", name))?;
1492
1493    profile_manager.save()?;
1494    println!("āœ… Removed {name} from profile {profile}");
1495    Ok(())
1496}
1497
1498fn handle_profile_delete(profile_manager: &mut ProfileManager, name: &str, force: bool) -> Result<()> {
1499    if !force {
1500        print!("āš ļø  Delete profile '{name}'? [y/N] ");
1501        std::io::Write::flush(&mut std::io::stdout())?;
1502
1503        let mut input = String::new();
1504        std::io::stdin().read_line(&mut input)?;
1505        if !input.trim().eq_ignore_ascii_case("y") {
1506            println!("Cancelled.");
1507            return Ok(());
1508        }
1509    }
1510
1511    profile_manager.delete(name)?;
1512    println!("āœ… Deleted profile: {name}");
1513    Ok(())
1514}
1515
1516fn handle_profile_export(profile_manager: &ProfileManager, name: &str, output: Option<String>) -> Result<()> {
1517    let json = profile_manager.export(name)?;
1518
1519    if let Some(path) = output {
1520        std::fs::write(path, json)?;
1521        println!("āœ… Exported profile to file");
1522    } else {
1523        println!("{json}");
1524    }
1525    Ok(())
1526}
1527
1528fn handle_profile_import(
1529    profile_manager: &mut ProfileManager,
1530    file: &str,
1531    name: Option<String>,
1532    overwrite: bool,
1533) -> Result<()> {
1534    let content = std::fs::read_to_string(file)?;
1535    let import_name = name.unwrap_or_else(|| "imported".to_string());
1536
1537    profile_manager.import(import_name.clone(), &content, overwrite)?;
1538    println!("āœ… Imported profile: {import_name}");
1539    Ok(())
1540}
1541
1542fn handle_profile_apply(
1543    profile_manager: &mut ProfileManager,
1544    env_manager: &mut EnvVarManager,
1545    name: &str,
1546) -> Result<()> {
1547    profile_manager.apply(name, env_manager)?;
1548    println!("āœ… Applied profile: {name}");
1549    Ok(())
1550}
1551
1552fn print_statistics(
1553    manager: &EnvVarManager,
1554    filtered_vars: &[&envx_core::EnvVar],
1555    total_count: usize,
1556    query: Option<&str>,
1557    source: Option<&str>,
1558) {
1559    let _term = Term::stdout();
1560
1561    // Count by source
1562    let system_count = manager.filter_by_source(&envx_core::EnvVarSource::System).len();
1563    let user_count = manager.filter_by_source(&envx_core::EnvVarSource::User).len();
1564    let process_count = manager.filter_by_source(&envx_core::EnvVarSource::Process).len();
1565    let shell_count = manager.filter_by_source(&envx_core::EnvVarSource::Shell).len();
1566
1567    // Header
1568    println!("{}", style("═".repeat(60)).blue().bold());
1569    println!("{}", style("Environment Variables Summary").cyan().bold());
1570    println!("{}", style("═".repeat(60)).blue().bold());
1571
1572    // Filter info
1573    if query.is_some() || source.is_some() {
1574        print!("  {} ", style("Filter:").yellow());
1575        if let Some(q) = query {
1576            print!("query='{}' ", style(q).green());
1577        }
1578        if let Some(s) = source {
1579            print!("source={} ", style(s).green());
1580        }
1581        println!();
1582        println!(
1583            "  {} {}/{} variables",
1584            style("Showing:").yellow(),
1585            style(filtered_vars.len()).green().bold(),
1586            total_count
1587        );
1588    } else {
1589        println!(
1590            "  {} {} variables",
1591            style("Total:").yellow(),
1592            style(total_count).green().bold()
1593        );
1594    }
1595
1596    println!();
1597    println!("  {} By Source:", style("ā–ŗ").cyan());
1598
1599    // Source breakdown with visual bars
1600    let max_count = system_count.max(user_count).max(process_count).max(shell_count);
1601    let bar_width = 30;
1602
1603    print_source_bar("System", system_count, max_count, bar_width, "red");
1604    print_source_bar("User", user_count, max_count, bar_width, "yellow");
1605    print_source_bar("Process", process_count, max_count, bar_width, "green");
1606    print_source_bar("Shell", shell_count, max_count, bar_width, "cyan");
1607
1608    println!("{}", style("─".repeat(60)).blue());
1609    println!();
1610}
1611
1612fn print_source_bar(label: &str, count: usize, max: usize, width: usize, color: &str) {
1613    let filled = if max > 0 { (count * width / max).max(1) } else { 0 };
1614
1615    let bar = "ā–ˆ".repeat(filled);
1616    let empty = "ā–‘".repeat(width - filled);
1617
1618    let colored_bar = match color {
1619        "red" => style(bar).red(),
1620        "yellow" => style(bar).yellow(),
1621        "green" => style(bar).green(),
1622        "cyan" => style(bar).cyan(),
1623        _ => style(bar).white(),
1624    };
1625
1626    println!(
1627        "    {:10} {} {}{} {}",
1628        style(label).bold(),
1629        colored_bar,
1630        style(empty).dim(),
1631        style(format!(" {count:4}")).bold(),
1632        style("vars").dim()
1633    );
1634}
1635
1636fn print_table(vars: Vec<&envx_core::EnvVar>, _is_limited: bool) {
1637    if vars.is_empty() {
1638        println!("{}", style("No environment variables found.").yellow());
1639    }
1640
1641    let mut table = Table::new();
1642
1643    // Configure table style
1644    table
1645        .set_content_arrangement(ContentArrangement::Dynamic)
1646        .set_width(120)
1647        .set_header(vec![
1648            Cell::new("Source").add_attribute(Attribute::Bold).fg(Color::Cyan),
1649            Cell::new("Name").add_attribute(Attribute::Bold).fg(Color::Cyan),
1650            Cell::new("Value").add_attribute(Attribute::Bold).fg(Color::Cyan),
1651        ]);
1652
1653    // Add rows with colored source indicators
1654    for var in vars {
1655        let (source_str, source_color) = format_source(&var.source);
1656        let truncated_value = truncate_value(&var.value, 50);
1657
1658        table.add_row(vec![
1659            Cell::new(source_str).fg(source_color),
1660            Cell::new(&var.name).fg(Color::White),
1661            Cell::new(truncated_value).fg(Color::Grey),
1662        ]);
1663    }
1664
1665    println!("{table}");
1666}
1667
1668fn format_source(source: &envx_core::EnvVarSource) -> (String, Color) {
1669    match source {
1670        envx_core::EnvVarSource::System => ("System".to_string(), Color::Red),
1671        envx_core::EnvVarSource::User => ("User".to_string(), Color::Yellow),
1672        envx_core::EnvVarSource::Process => ("Process".to_string(), Color::Green),
1673        envx_core::EnvVarSource::Shell => ("Shell".to_string(), Color::Cyan),
1674        envx_core::EnvVarSource::Application(app) => (format!("App:{app}"), Color::Magenta),
1675    }
1676}
1677
1678fn format_source_compact(source: &envx_core::EnvVarSource) -> console::StyledObject<String> {
1679    match source {
1680        envx_core::EnvVarSource::System => style("[SYS]".to_string()).red().bold(),
1681        envx_core::EnvVarSource::User => style("[USR]".to_string()).yellow().bold(),
1682        envx_core::EnvVarSource::Process => style("[PRC]".to_string()).green().bold(),
1683        envx_core::EnvVarSource::Shell => style("[SHL]".to_string()).cyan().bold(),
1684        envx_core::EnvVarSource::Application(app) => style(format!("[{}]", &app[..3.min(app.len())].to_uppercase()))
1685            .magenta()
1686            .bold(),
1687    }
1688}
1689
1690fn truncate_value(value: &str, max_len: usize) -> String {
1691    if value.len() <= max_len {
1692        value.to_string()
1693    } else {
1694        format!("{}...", &value[..max_len - 3])
1695    }
1696}