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 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 let command_str = command.iter().enumerate().fold(String::new(), |mut acc, (i, arg)| {
121 if i > 0 {
122 acc.push(' ');
123 }
124 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 if command_str.trim().is_empty() {
135 return Err(anyhow!("Cannot add empty command"));
136 }
137
138 let directory = std::env::current_dir()?
140 .to_string_lossy()
141 .to_string();
142
143 let timestamp = Local::now().with_timezone(&Utc);
144
145 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 !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 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 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, ¤t_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!(); 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 if let Some(command) = db.get_command(command_id)? {
285 println!("Deleting command:");
287 print_commands(&[command])?;
288
289 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}