command_vault/cli/
commands.rs

1use anyhow::{Result, anyhow};
2use chrono::{Local, Utc};
3use std::io::{self, Stdout};
4use crossterm::{
5    execute,
6    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use ratatui::{
9    backend::CrosstermBackend,
10    layout::{Constraint, Direction, Layout},
11    style::{Color, Style},
12    text::{Line, Span},
13    widgets::{Block, Borders, Paragraph},
14    Terminal,
15};
16use colored::*;
17
18use crate::db::{Command, Database};
19use crate::ui::App;
20use crate::utils::params::parse_parameters;
21use crate::utils::params::substitute_parameters;
22use crate::exec::{ExecutionContext, execute_shell_command};
23
24use super::args::{Commands, TagCommands};
25
26fn print_commands(commands: &[Command]) -> Result<()> {
27    let terminal_result = setup_terminal();
28    
29    match terminal_result {
30        Ok(mut terminal) => {
31            let res = print_commands_ui(&mut terminal, commands);
32            restore_terminal(&mut terminal)?;
33            res
34        }
35        Err(_) => {
36            // Fallback to simple text output
37            println!("Command History:");
38            println!("─────────────────────────────────────────────");
39            for cmd in commands {
40                let local_time = cmd.timestamp.with_timezone(&Local);
41                println!("{} │ {}", local_time.format("%Y-%m-%d %H:%M:%S"), cmd.command);
42                if !cmd.tags.is_empty() {
43                    println!("    Tags: {}", cmd.tags.join(", "));
44                }
45                if !cmd.parameters.is_empty() {
46                    println!("    Parameters:");
47                    for param in &cmd.parameters {
48                        let desc = param.description.as_deref().unwrap_or("None");
49                        println!("      - {}: {} (default: {})", param.name, desc, "None");
50                    }
51                }
52                println!("    Directory: {}", cmd.directory);
53                println!();
54            }
55            Ok(())
56        }
57    }
58}
59
60fn print_commands_ui(terminal: &mut Terminal<CrosstermBackend<Stdout>>, commands: &[Command]) -> Result<()> {
61    terminal.draw(|f| {
62        let chunks = Layout::default()
63            .direction(Direction::Vertical)
64            .margin(1)
65            .constraints([Constraint::Min(0)])
66            .split(f.size());
67
68        let mut lines = vec![];
69        lines.push(Line::from(Span::styled(
70            "Command History:",
71            Style::default().fg(Color::Cyan),
72        )));
73        lines.push(Line::from(Span::raw("─────────────────────────────────────────────")));
74
75        for cmd in commands {
76            let local_time = cmd.timestamp.with_timezone(&Local);
77            lines.push(Line::from(vec![
78                Span::styled(local_time.format("%Y-%m-%d %H:%M:%S").to_string(), Style::default().fg(Color::Yellow)),
79                Span::raw(" │ "),
80                Span::raw(&cmd.command),
81            ]));
82            lines.push(Line::from(vec![
83                Span::raw("    Directory: "),
84                Span::raw(&cmd.directory),
85            ]));
86            if !cmd.tags.is_empty() {
87                lines.push(Line::from(vec![
88                    Span::raw("    Tags: "),
89                    Span::raw(cmd.tags.join(", ")),
90                ]));
91            }
92            lines.push(Line::from(Span::raw("─────────────────────────────────────────────")));
93        }
94
95        let paragraph = Paragraph::new(lines).block(Block::default().borders(Borders::ALL));
96        f.render_widget(paragraph, chunks[0]);
97    })?;
98    Ok(())
99}
100
101fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
102    enable_raw_mode()?;
103    let mut stdout = io::stdout();
104    execute!(stdout, EnterAlternateScreen)?;
105    let backend = CrosstermBackend::new(stdout);
106    Terminal::new(backend).map_err(|e| e.into())
107}
108
109fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
110    disable_raw_mode()?;
111    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
112    terminal.show_cursor()?;
113    Ok(())
114}
115
116pub fn handle_command(command: Commands, db: &mut Database, debug: bool) -> Result<()> {
117    match command {
118        Commands::Add { command, tags } => {
119            // Process command parts with special handling for git format strings
120            let command_str = command.iter().enumerate().fold(String::new(), |mut acc, (i, arg)| {
121                if i > 0 {
122                    acc.push(' ');
123                }
124                // Special case for git format strings
125                if arg.starts_with("--pretty=format:") {
126                    acc.push_str(&format!("\"{}\"", arg));
127                } else {
128                    acc.push_str(arg);
129                }
130                acc
131            });
132            
133            // Don't allow empty commands
134            if command_str.trim().is_empty() {
135                return Err(anyhow!("Cannot add empty command"));
136            }
137            
138            // Get the current directory
139            let directory = std::env::current_dir()?
140                .to_string_lossy()
141                .to_string();
142            
143            let timestamp = Local::now().with_timezone(&Utc);
144            
145            // Parse parameters from command string
146            let parameters = parse_parameters(&command_str);
147            
148            let cmd = Command {
149                id: None,
150                command: command_str.clone(),
151                timestamp,
152                directory,
153                tags,
154                parameters,
155            };
156            let id = db.add_command(&cmd)?;
157            println!("Command added to history with ID: {}", id);
158            
159            // If command has parameters, show them
160            if !cmd.parameters.is_empty() {
161                println!("\nDetected parameters:");
162                for param in &cmd.parameters {
163                    let desc = param.description.as_deref().unwrap_or("None");
164                    println!("  {} - Description: {}", param.name.yellow(), desc);
165                }
166            }
167        }
168        Commands::Search { query, limit } => {
169            let commands = db.search_commands(&query, limit)?;
170            let mut app = App::new(commands.clone(), db, debug);
171            match app.run() {
172                Ok(_) => (),
173                Err(e) => {
174                    if e.to_string() == "Operation cancelled by user" {
175                        print!("\n{}", "Operation cancelled.".yellow());
176                        return Ok(());
177                    }
178                    eprintln!("Failed to start TUI mode: {}", e);
179                    print_commands(&commands)?;
180                }
181            }
182        }
183        Commands::Ls { limit, asc } => {
184            let commands = db.list_commands(limit, asc)?;
185            if commands.is_empty() {
186                print!("No commands found.");
187                return Ok(());
188            }
189
190            // Check if TUI should be disabled (useful for testing or non-interactive environments)
191            if std::env::var("COMMAND_VAULT_NO_TUI").is_ok() {
192                for cmd in commands {
193                    print!("{}: {} ({})", cmd.id.unwrap_or(0), cmd.command, cmd.directory);
194                }
195                return Ok(());
196            }
197
198            let mut app = App::new(commands.clone(), db, debug);
199            match app.run() {
200                Ok(_) => (),
201                Err(e) => {
202                    if e.to_string() == "Operation cancelled by user" {
203                        print!("\n{}", "Operation cancelled.".yellow());
204                        return Ok(());
205                    }
206                    eprintln!("Failed to start TUI mode: {}", e);
207                    print_commands(&commands)?;
208                }
209            }
210        }
211        Commands::Tag { action } => match action {
212            TagCommands::Add { command_id, tags } => {
213                match db.add_tags_to_command(command_id, &tags) {
214                    Ok(_) => print!("Tags added successfully"),
215                    Err(e) => eprintln!("Failed to add tags: {}", e),
216                }
217            }
218            TagCommands::Remove { command_id, tag } => {
219                match db.remove_tag_from_command(command_id, &tag) {
220                    Ok(_) => print!("Tag removed successfully"),
221                    Err(e) => eprintln!("Failed to remove tag: {}", e),
222                }
223            }
224            TagCommands::List => {
225                match db.list_tags() {
226                    Ok(tags) => {
227                        if tags.is_empty() {
228                            print!("No tags found");
229                            return Ok(());
230                        }
231                        
232                        print!("\nTags and their usage:");
233                        print!("─────────────────────────────────────────────");
234                        for (tag, count) in tags {
235                            print!("{}: {} command{}", tag, count, if count == 1 { "" } else { "s" });
236                        }
237                    }
238                    Err(e) => eprintln!("Failed to list tags: {}", e),
239                }
240            }
241            TagCommands::Search { tag, limit } => {
242                match db.search_by_tag(&tag, limit) {
243                    Ok(commands) => print_commands(&commands)?,
244                    Err(e) => eprintln!("Failed to search by tag: {}", e),
245                }
246            }
247        },
248        Commands::Exec { command_id, debug } => {
249            let command = db.get_command(command_id)?
250                .ok_or_else(|| anyhow!("Command not found with ID: {}", command_id))?;
251            
252            // Create the directory if it doesn't exist
253            if !std::path::Path::new(&command.directory).exists() {
254                std::fs::create_dir_all(&command.directory)?;
255            }
256            
257            let current_params = parse_parameters(&command.command);
258            let final_command = substitute_parameters(&command.command, &current_params, None)?;
259
260            let ctx = ExecutionContext {
261                command: final_command.clone(),
262                directory: command.directory.clone(),
263                test_mode: std::env::var("COMMAND_VAULT_TEST").is_ok(),
264                debug_mode: debug,
265            };
266
267            println!("\n─────────────────────────────────────────────");
268            println!("Command to execute: {}", final_command);
269            println!("Working directory: {}", command.directory);
270            println!();  // Add extra newline before command output
271
272            execute_shell_command(&ctx)?;
273        }
274        Commands::ShellInit { shell } => {
275            let script_path = crate::shell::hooks::init_shell(shell)?;
276            if !script_path.exists() {
277                return Err(anyhow!("Shell integration script not found at: {}", script_path.display()));
278            }
279            print!("{}", script_path.display());
280            return Ok(());
281        },
282        Commands::Delete { command_id } => {
283            // First check if the command exists
284            if let Some(command) = db.get_command(command_id)? {
285                // Show the command that will be deleted
286                println!("Deleting command:");
287                print_commands(&[command])?;
288                
289                // Delete the command
290                db.delete_command(command_id)?;
291                println!("Command deleted successfully");
292            } else {
293                return Err(anyhow!("Command with ID {} not found", command_id));
294            }
295        }
296    }
297    Ok(())
298}