doum_cli/cli/
ui.rs

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// UI module for all user interactions (ask, suggest modes)
10// Config mode uses TUI (tui.rs, menu.rs)
11
12/// Action to take after selecting a command
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum CommandAction {
15    Copy,
16    Execute,
17    Cancel,
18}
19
20/// Enhanced command selection with dialoguer
21pub 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    // Format items with command in color and description
32    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            // Ask what to do with the command
56            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
81/// Simple confirmation prompt
82pub 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
95/// Text input prompt
96pub 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
111/// Password input prompt
112pub 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
121/// Number input prompt
122pub 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
141/// Copy text to clipboard
142pub 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
153/// Create a spinner with message
154pub 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
167/// Finish spinner
168pub 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}