systemprompt_logging/services/cli/
prompts.rs1#![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}