1use crate::chrome::{ChromeStatus, ChromeTitle};
4use crate::report::ReportCommand;
5use crate::wizard::WizardCommand;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ButtonsPreset {
10 Ok,
12 OkCancel,
14 YesNo,
16 YesNoCancel,
18 RetryCancel,
20 Custom,
22}
23
24impl ButtonsPreset {
25 pub fn parse(value: &str) -> Option<Self> {
27 match value {
28 "ok" => Some(Self::Ok),
29 "ok_cancel" => Some(Self::OkCancel),
30 "yes_no" => Some(Self::YesNo),
31 "yes_no_cancel" => Some(Self::YesNoCancel),
32 "retry_cancel" => Some(Self::RetryCancel),
33 "custom" => Some(Self::Custom),
34 _ => None,
35 }
36 }
37
38 pub fn all_names() -> &'static [&'static str] {
40 &[
41 "ok",
42 "ok_cancel",
43 "yes_no",
44 "yes_no_cancel",
45 "retry_cancel",
46 "custom",
47 ]
48 }
49
50 pub fn display_labels(self, custom_buttons: Option<&[String]>) -> Vec<String> {
52 match self {
53 Self::Ok => vec!["OK".into()],
54 Self::OkCancel => vec!["OK".into(), "Cancel".into()],
55 Self::YesNo => vec!["Yes".into(), "No".into()],
56 Self::YesNoCancel => vec!["Yes".into(), "No".into(), "Cancel".into()],
57 Self::RetryCancel => vec!["Retry".into(), "Cancel".into()],
58 Self::Custom => custom_buttons.unwrap_or(&[]).to_vec(),
59 }
60 }
61
62 pub fn wire_labels(self, custom_buttons: Option<&[String]>) -> Vec<String> {
64 match self {
65 Self::Ok => vec!["ok".into()],
66 Self::OkCancel => vec!["ok".into(), "cancel".into()],
67 Self::YesNo => vec!["yes".into(), "no".into()],
68 Self::YesNoCancel => vec!["yes".into(), "no".into(), "cancel".into()],
69 Self::RetryCancel => vec!["retry".into(), "cancel".into()],
70 Self::Custom => custom_buttons.unwrap_or(&[]).to_vec(),
71 }
72 }
73
74 pub fn button_count(self, custom_buttons: Option<&[String]>) -> usize {
76 self.wire_labels(custom_buttons).len()
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum MessageLevel {
83 Info,
85 Warning,
87 Error,
89 Question,
91}
92
93impl MessageLevel {
94 pub fn parse(value: &str) -> Option<Self> {
96 match value {
97 "info" => Some(Self::Info),
98 "warning" => Some(Self::Warning),
99 "error" => Some(Self::Error),
100 "question" => Some(Self::Question),
101 _ => None,
102 }
103 }
104
105 pub fn all_names() -> &'static [&'static str] {
107 &["info", "warning", "error", "question"]
108 }
109
110 pub fn as_str(self) -> &'static str {
112 match self {
113 Self::Info => "info",
114 Self::Warning => "warning",
115 Self::Error => "error",
116 Self::Question => "question",
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum InputMode {
124 Text,
126 File,
128 Folder,
130}
131
132impl InputMode {
133 pub fn parse(value: &str) -> Option<Self> {
135 match value {
136 "text" => Some(Self::Text),
137 "file" => Some(Self::File),
138 "folder" => Some(Self::Folder),
139 _ => None,
140 }
141 }
142
143 pub fn all_names() -> &'static [&'static str] {
145 &["text", "file", "folder"]
146 }
147
148 pub fn as_str(self) -> &'static str {
150 match self {
151 Self::Text => "text",
152 Self::File => "file",
153 Self::Folder => "folder",
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
160pub struct WindowSizeHint {
161 pub width: Option<u32>,
163 pub height: Option<u32>,
165}
166
167impl WindowSizeHint {
168 pub fn is_some(&self) -> bool {
170 self.width.is_some() || self.height.is_some()
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum Command {
177 Chrome {
179 title: ChromeTitle,
180 status: Option<ChromeStatus>,
181 width: Option<u32>,
182 height: Option<u32>,
183 },
184 Message {
186 title: ChromeTitle,
187 message: String,
188 status: Option<ChromeStatus>,
189 buttons: ButtonsPreset,
190 custom_buttons: Option<Vec<String>>,
191 default_button: Option<u32>,
192 level: Option<MessageLevel>,
193 icon: Option<crate::MediaRef>,
194 image: Option<crate::MediaRef>,
195 markdown: bool,
196 width: Option<u32>,
197 height: Option<u32>,
198 },
199 Input {
201 title: ChromeTitle,
202 message: String,
203 status: Option<ChromeStatus>,
204 icon: Option<crate::MediaRef>,
205 markdown: bool,
206 multiline: bool,
207 placeholder: Option<String>,
208 default: Option<String>,
209 password: bool,
211 mode: InputMode,
212 filter: Option<Vec<String>>,
214 multiple: bool,
216 start_path: Option<String>,
218 buttons: ButtonsPreset,
219 width: Option<u32>,
220 height: Option<u32>,
221 },
222 Markdown {
224 title: Option<ChromeTitle>,
226 file: Option<String>,
228 content: Option<String>,
230 status: Option<ChromeStatus>,
231 buttons: ButtonsPreset,
233 width: Option<u32>,
234 height: Option<u32>,
235 },
236 Question {
238 questions: Vec<QuestionCard>,
240 questions_raw: Vec<serde_json::Value>,
242 width: Option<u32>,
243 height: Option<u32>,
244 },
245 Wizard(WizardCommand),
247 Report(ReportCommand),
249}
250
251impl Command {
252 pub fn window_width(&self) -> Option<u32> {
254 match self {
255 Self::Chrome { width, .. }
256 | Self::Message { width, .. }
257 | Self::Input { width, .. }
258 | Self::Markdown { width, .. }
259 | Self::Question { width, .. } => *width,
260 Self::Wizard(cmd) => cmd.width,
261 Self::Report(cmd) => cmd.width,
262 }
263 }
264
265 pub fn window_height(&self) -> Option<u32> {
267 match self {
268 Self::Chrome { height, .. }
269 | Self::Message { height, .. }
270 | Self::Input { height, .. }
271 | Self::Markdown { height, .. }
272 | Self::Question { height, .. } => *height,
273 Self::Wizard(cmd) => cmd.height,
274 Self::Report(cmd) => cmd.height,
275 }
276 }
277
278 pub fn window_size_hint(&self) -> WindowSizeHint {
280 WindowSizeHint {
281 width: self.window_width(),
282 height: self.window_height(),
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct QuestionOption {
290 pub label: String,
292 pub description: String,
294 pub preview: Option<String>,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum QuestionPromptError {
301 Empty,
303}
304
305impl std::fmt::Display for QuestionPromptError {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 match self {
308 Self::Empty => f.write_str("question prompt must be a non-empty string"),
309 }
310 }
311}
312
313impl std::error::Error for QuestionPromptError {}
314
315#[derive(Debug, Clone, PartialEq, Eq, Hash)]
320pub struct QuestionPrompt(String);
321
322impl QuestionPrompt {
323 pub fn new(value: impl Into<String>) -> Self {
327 Self(value.into())
328 }
329
330 pub fn try_new(value: impl Into<String>) -> Result<Self, QuestionPromptError> {
336 let value = value.into();
337 if value.is_empty() {
338 return Err(QuestionPromptError::Empty);
339 }
340 Ok(Self(value))
341 }
342
343 pub fn as_str(&self) -> &str {
345 &self.0
346 }
347
348 pub fn into_inner(self) -> String {
350 self.0
351 }
352}
353
354impl std::ops::Deref for QuestionPrompt {
355 type Target = str;
356
357 fn deref(&self) -> &Self::Target {
358 &self.0
359 }
360}
361
362impl AsRef<str> for QuestionPrompt {
363 fn as_ref(&self) -> &str {
364 self.as_str()
365 }
366}
367
368impl std::fmt::Display for QuestionPrompt {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 self.0.fmt(f)
371 }
372}
373
374impl From<String> for QuestionPrompt {
375 fn from(value: String) -> Self {
376 Self::new(value)
377 }
378}
379
380impl From<&str> for QuestionPrompt {
381 fn from(value: &str) -> Self {
382 Self::new(value)
383 }
384}
385
386impl PartialEq<str> for QuestionPrompt {
387 fn eq(&self, other: &str) -> bool {
388 self.0 == other
389 }
390}
391
392impl PartialEq<&str> for QuestionPrompt {
393 fn eq(&self, other: &&str) -> bool {
394 self.0 == *other
395 }
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
400pub struct QuestionCard {
401 pub question: QuestionPrompt,
403 pub header: String,
405 pub options: Vec<QuestionOption>,
407 pub multi_select: bool,
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn preset_label_mapping_table() {
417 assert_eq!(ButtonsPreset::Ok.display_labels(None), ["OK"]);
418 assert_eq!(ButtonsPreset::Ok.wire_labels(None), ["ok"]);
419
420 assert_eq!(
421 ButtonsPreset::OkCancel.display_labels(None),
422 ["OK", "Cancel"]
423 );
424 assert_eq!(ButtonsPreset::OkCancel.wire_labels(None), ["ok", "cancel"]);
425
426 assert_eq!(ButtonsPreset::YesNo.display_labels(None), ["Yes", "No"]);
427 assert_eq!(ButtonsPreset::YesNo.wire_labels(None), ["yes", "no"]);
428
429 assert_eq!(
430 ButtonsPreset::YesNoCancel.display_labels(None),
431 ["Yes", "No", "Cancel"]
432 );
433 assert_eq!(
434 ButtonsPreset::YesNoCancel.wire_labels(None),
435 ["yes", "no", "cancel"]
436 );
437
438 assert_eq!(
439 ButtonsPreset::RetryCancel.display_labels(None),
440 ["Retry", "Cancel"]
441 );
442 assert_eq!(
443 ButtonsPreset::RetryCancel.wire_labels(None),
444 ["retry", "cancel"]
445 );
446 }
447
448 #[test]
449 fn custom_labels_are_verbatim() {
450 let custom = vec!["Save".into(), "Discard".into()];
451 assert_eq!(ButtonsPreset::Custom.display_labels(Some(&custom)), custom);
452 assert_eq!(ButtonsPreset::Custom.wire_labels(Some(&custom)), custom);
453 }
454
455 #[test]
456 fn question_prompt_try_new_rejects_empty() {
457 assert_eq!(QuestionPrompt::try_new(""), Err(QuestionPromptError::Empty));
458 assert_eq!(QuestionPrompt::try_new("Q?").unwrap().as_str(), "Q?");
459 }
460}