1use crate::system::error::{DoumError, Result};
2use crate::llm::CommandSuggestion;
3use arboard::Clipboard;
4use indicatif::{ProgressBar, ProgressStyle};
5use dialoguer::{Select, Confirm, Input, Password, theme::ColorfulTheme};
6use console::Style;
7use std::time::Duration;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum CommandAction {
15 Copy,
16 Execute,
17 Cancel,
18}
19
20pub fn prompt_for_command_selection(suggestions: &[CommandSuggestion]) -> Result<Option<(usize, CommandAction)>> {
22 if suggestions.is_empty() {
23 println!("\n⚠️ No commands to suggest.");
24 return Ok(None);
25 }
26
27 let theme = ColorfulTheme::default();
28 let cmd_style = Style::new().cyan().bold();
29 let desc_style = Style::new().dim();
30
31 let items: Vec<String> = suggestions
33 .iter()
34 .map(|s| {
35 format!(
36 "{}\n {}",
37 cmd_style.apply_to(&s.cmd),
38 desc_style.apply_to(&s.description)
39 )
40 })
41 .collect();
42
43 println!("\n📋 Select a command:\n");
44
45 let selection = Select::with_theme(&theme)
46 .items(&items)
47 .default(0)
48 .interact_opt()
49 .map_err(|e| DoumError::Config(format!("Selection failed: {}", e)))?;
50
51 match selection {
52 Some(index) => {
53 let selected_cmd = &suggestions[index].cmd;
54
55 println!("\n📋 Selected: {}", cmd_style.apply_to(selected_cmd));
57
58 let actions = vec![
59 "📋 Copy to clipboard",
60 "▶️ Execute now",
61 "❌ Cancel",
62 ];
63
64 let action = Select::with_theme(&theme)
65 .with_prompt("What would you like to do?")
66 .items(&actions)
67 .default(0)
68 .interact_opt()
69 .map_err(|e| DoumError::Config(format!("Action selection failed: {}", e)))?;
70
71 match action {
72 Some(0) => Ok(Some((index, CommandAction::Copy))),
73 Some(1) => Ok(Some((index, CommandAction::Execute))),
74 _ => Ok(Some((index, CommandAction::Cancel))),
75 }
76 }
77 None => Ok(None),
78 }
79}
80
81pub fn confirm_execution(command: &str) -> Result<bool> {
83 let theme = ColorfulTheme::default();
84 let cmd_style = Style::new().cyan().bold();
85
86 println!("\n📋 Command: {}", cmd_style.apply_to(command));
87
88 Confirm::with_theme(&theme)
89 .with_prompt("Execute this command?")
90 .default(true)
91 .interact()
92 .map_err(|e| DoumError::Config(format!("Confirmation failed: {}", e)))
93}
94
95pub fn prompt_text_input(message: &str, default: Option<&str>) -> Result<String> {
97 let theme = ColorfulTheme::default();
98
99 let mut input = Input::with_theme(&theme)
100 .with_prompt(message);
101
102 if let Some(def) = default {
103 input = input.default(def.to_string());
104 }
105
106 input
107 .interact_text()
108 .map_err(|e| DoumError::Config(format!("Input failed: {}", e)))
109}
110
111pub fn prompt_password_input(message: &str) -> Result<String> {
113 let theme = ColorfulTheme::default();
114
115 Password::with_theme(&theme)
116 .with_prompt(message)
117 .interact()
118 .map_err(|e| DoumError::Config(format!("Password input failed: {}", e)))
119}
120
121pub fn prompt_number_input<T>(message: &str, default: Option<T>) -> Result<T>
123where
124 T: std::str::FromStr + std::fmt::Display + Clone,
125 T::Err: std::fmt::Display,
126{
127 let theme = ColorfulTheme::default();
128
129 let mut input = Input::with_theme(&theme)
130 .with_prompt(message);
131
132 if let Some(def) = default {
133 input = input.default(def);
134 }
135
136 input
137 .interact_text()
138 .map_err(|e| DoumError::Config(format!("Number input failed: {}", e)))
139}
140
141pub fn copy_to_clipboard(text: &str) -> Result<()> {
143 let mut clipboard = Clipboard::new()
144 .map_err(|e| DoumError::Config(format!("Clipboard init failed: {}", e)))?;
145
146 clipboard
147 .set_text(text)
148 .map_err(|e| DoumError::Config(format!("Clipboard copy failed: {}", e)))?;
149
150 Ok(())
151}
152
153pub fn create_spinner(message: &str) -> ProgressBar {
155 let spinner = ProgressBar::new_spinner();
156 spinner.set_style(
157 ProgressStyle::default_spinner()
158 .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
159 .template("{spinner:.cyan} {msg}")
160 .unwrap()
161 );
162 spinner.set_message(message.to_string());
163 spinner.enable_steady_tick(Duration::from_millis(80));
164 spinner
165}
166
167pub fn finish_spinner(spinner: ProgressBar, message: Option<&str>) {
169 if let Some(msg) = message {
170 spinner.finish_with_message(msg.to_string());
171 } else {
172 spinner.finish_and_clear();
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_copy_to_clipboard() {
182 let result = copy_to_clipboard("test command");
183 assert!(result.is_ok() || result.is_err());
184 }
185}