doum_cli/cli/
ui.rs

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/// Action to take after selecting a command
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum CommandAction {
12    Copy,
13    Execute,
14    Cancel,
15}
16
17/// Enhanced command selection with dialoguer
18pub 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    // Format items with command in color and description
31    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            // Ask what to do with the command
55            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
76/// Simple confirmation prompt
77pub 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
90/// Text input prompt
91pub 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
107/// Password input prompt
108pub 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
117/// Number input prompt
118pub 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
136/// Copy text to clipboard
137pub 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
148/// Create a spinner with message
149pub 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
162/// Finish spinner
163pub 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}