Skip to main content

systemprompt_logging/services/cli/
prompts.rs

1#![allow(clippy::print_stdout)]
2
3use anyhow::Result;
4use dialoguer::theme::ColorfulTheme;
5use dialoguer::Confirm;
6
7use crate::services::cli::display::{CollectionDisplay, Display, DisplayUtils, StatusDisplay};
8use crate::services::cli::theme::MessageLevel;
9#[derive(Debug, Copy, Clone)]
10pub struct Prompts;
11
12impl Prompts {
13    pub fn confirm(message: &str, default: bool) -> Result<bool> {
14        let confirmation = Confirm::with_theme(&ColorfulTheme::default())
15            .with_prompt(message)
16            .default(default)
17            .interact()?;
18        Ok(confirmation)
19    }
20
21    pub fn confirm_schemas() -> Result<bool> {
22        Self::confirm("Apply these schemas?", true)
23    }
24
25    pub fn confirm_seeds() -> Result<bool> {
26        Self::confirm("Apply these seeds?", true)
27    }
28
29    pub fn confirm_install(modules: &[String]) -> Result<bool> {
30        if modules.is_empty() {
31            return Ok(false);
32        }
33
34        DisplayUtils::section_header("New modules found");
35
36        let displays: Vec<StatusDisplay> = modules
37            .iter()
38            .map(|name| {
39                StatusDisplay::new(crate::services::cli::theme::ItemStatus::Pending, name)
40                    .with_detail("ready to install")
41            })
42            .collect();
43
44        let collection = CollectionDisplay::new("Modules", displays).without_count();
45        collection.display();
46
47        Self::confirm("Install these modules?", false)
48    }
49
50    pub fn confirm_update(updates: &[(String, String, String)]) -> Result<bool> {
51        if updates.is_empty() {
52            return Ok(false);
53        }
54
55        DisplayUtils::section_header("Module updates available");
56
57        let displays: Vec<StatusDisplay> = updates
58            .iter()
59            .map(|(name, old, new)| {
60                let detail = format!("{old} → {new}");
61                StatusDisplay::new(crate::services::cli::theme::ItemStatus::Pending, name)
62                    .with_detail(detail)
63            })
64            .collect();
65
66        let collection = CollectionDisplay::new("Updates", displays).without_count();
67        collection.display();
68
69        Self::confirm("Update these modules?", false)
70    }
71
72    pub fn confirm_with_context<T: Display>(
73        context_items: &[T],
74        context_title: &str,
75        question: &str,
76        default: bool,
77    ) -> Result<bool> {
78        if !context_items.is_empty() {
79            DisplayUtils::section_header(context_title);
80            for item in context_items {
81                item.display();
82            }
83            println!();
84        }
85
86        Self::confirm(question, default)
87    }
88}
89
90pub struct PromptBuilder {
91    title: Option<String>,
92    message: String,
93    default: bool,
94    show_context: Vec<Box<dyn Display>>,
95}
96
97impl std::fmt::Debug for PromptBuilder {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("PromptBuilder")
100            .field("title", &self.title)
101            .field("message", &self.message)
102            .field("default", &self.default)
103            .field(
104                "show_context",
105                &format!("[{} items]", self.show_context.len()),
106            )
107            .finish()
108    }
109}
110
111impl PromptBuilder {
112    pub fn new(message: impl Into<String>) -> Self {
113        Self {
114            title: None,
115            message: message.into(),
116            default: false,
117            show_context: Vec::new(),
118        }
119    }
120
121    #[must_use]
122    pub fn with_title(mut self, title: impl Into<String>) -> Self {
123        self.title = Some(title.into());
124        self
125    }
126
127    #[must_use]
128    pub const fn with_default(mut self, default: bool) -> Self {
129        self.default = default;
130        self
131    }
132
133    #[must_use]
134    pub fn with_context<T: Display + 'static>(mut self, item: T) -> Self {
135        self.show_context.push(Box::new(item));
136        self
137    }
138
139    pub fn confirm(self) -> Result<bool> {
140        if let Some(title) = &self.title {
141            DisplayUtils::section_header(title);
142        }
143
144        for item in &self.show_context {
145            item.display();
146        }
147
148        if !self.show_context.is_empty() {
149            println!();
150        }
151
152        Prompts::confirm(&self.message, self.default)
153    }
154}
155
156#[derive(Debug, Copy, Clone)]
157pub struct QuickPrompts;
158
159impl QuickPrompts {
160    pub fn yes_no(question: &str) -> Result<bool> {
161        Prompts::confirm(question, false)
162    }
163
164    pub fn yes_no_default_yes(question: &str) -> Result<bool> {
165        Prompts::confirm(question, true)
166    }
167
168    pub fn continue_or_abort(action: &str) -> Result<bool> {
169        let message = format!("Continue with {action}?");
170        Prompts::confirm(&message, false)
171    }
172
173    pub fn dangerous_action(action: &str) -> Result<bool> {
174        let warning = format!("This will {action}");
175        DisplayUtils::message(MessageLevel::Warning, &warning);
176        Prompts::confirm("Are you sure?", false)
177    }
178}