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