1use std::{env, fmt, io, process};
2use std::io::{IsTerminal, Write};
3
4#[derive(Copy, Clone, Debug)]
5enum TestSelector {
6 AllTests,
7 MessageBox,
8 SaveFileDialog,
9 OpenFileDialog,
10 FolderDialog,
11 ColorPicker,
12 TextInput,
13 Notification,
14}
15
16fn main() {
17 print_environment();
18
19 let args: Vec<_> = env::args().skip(1).collect();
20 let args = args.iter().map(|s| s.as_str()).collect::<Vec<_>>();
21
22 let selector = if args.len() != 1 {
23 prompt_selector()
24 }
25 else if let Some(selector) = parse_selector(args[0]) {
26 selector
27 }
28 else {
29 prompt_selector()
30 };
31
32 match selector {
33 TestSelector::AllTests => {
34 test_message_box();
35 test_save_file_dialog();
36 test_open_file_dialog();
37 test_folder_dialog();
38 test_color_picker();
39 test_text_input();
40 test_notification();
41 }
42 TestSelector::MessageBox => test_message_box(),
43 TestSelector::SaveFileDialog => test_save_file_dialog(),
44 TestSelector::OpenFileDialog => test_open_file_dialog(),
45 TestSelector::FolderDialog => test_folder_dialog(),
46 TestSelector::ColorPicker => test_color_picker(),
47 TestSelector::TextInput => test_text_input(),
48 TestSelector::Notification => test_notification(),
49 }
50}
51
52fn print_environment() {
53 println!("\n{}", Color("Environment", "255;214;102"));
54 println!(" {}: {}", Color("OS", "170;170;170"), env::consts::OS);
55 println!(" {}: {}", Color("Arch", "170;170;170"), env::consts::ARCH);
56 let backend = env::var("RUSTY_DIALOGS_BACKEND");
57 let backend = match &backend { Ok(value) => value as &dyn fmt::Display, Err(_) => &"(not set)" as &dyn fmt::Display };
58 println!(" {}: {}", Color("RUSTY_DIALOGS_BACKEND", "170;170;170"), backend);
59 println!(" {}: {}", Color("rustc", "170;170;170"), version_command("rustc", "-V"));
60 println!(" {}: {}", Color("cargo", "170;170;170"), version_command("cargo", "-V"));
61}
62
63fn version_command(cmd: &str, arg: &str) -> String {
64 match process::Command::new(cmd).arg(arg).output() {
65 Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_string(),
66 Ok(out) => format!("(failed: exit status {})", out.status),
67 Err(err) => format!("(not available: {err})"),
68 }
69}
70
71fn prompt_selector() -> TestSelector {
72 println!("\n{}", Color("Select tests", "255;214;102"));
73 println!(" Enter = all tests");
74 println!(" m = MessageBox");
75 println!(" s = SaveFileDialog");
76 println!(" o = OpenFileDialog");
77 println!(" f = FolderDialog");
78 println!(" t = TextInput");
79 println!(" c = ColorPicker");
80 println!(" n = Notification");
81
82 loop {
83 let value = prompt_input("Choice: ");
84 match parse_selector(value.trim()) {
85 Some(selector) => return selector,
86 None => eprintln!("{}", Color("Invalid choice, please try again.", "255;107;107")),
87 }
88 }
89}
90
91fn prompt_input(prompt: &str) -> String {
92 print!("{prompt}");
93 _ = io::stdout().flush();
94 let mut line = String::new();
95 if io::stdin().read_line(&mut line).is_err() {
96 return String::new();
97 }
98 line
99}
100
101fn parse_selector(s: &str) -> Option<TestSelector> {
102 match s {
103 "" | "all" | "Enter" => Some(TestSelector::AllTests),
104 "m" => Some(TestSelector::MessageBox),
105 "s" => Some(TestSelector::SaveFileDialog),
106 "o" => Some(TestSelector::OpenFileDialog),
107 "f" => Some(TestSelector::FolderDialog),
108 "c" => Some(TestSelector::ColorPicker),
109 "t" => Some(TestSelector::TextInput),
110 "n" => Some(TestSelector::Notification),
111 _ => None,
112 }
113}
114
115fn step<F: Fn() -> T, T: fmt::Debug + PartialEq>(description: &str, expected: T, action: F) {
116 println!("\n{} {description}", Color("Step:", "255;214;102"));
117 loop {
118 let result = action();
119 if result == expected {
120 println!(" Result: {}", Color("PASS", "123;201;111"));
121 break;
122 }
123
124 println!(" Result: {} - expected {expected:?}, got {result:?}", Color("FAIL", "255;107;107"));
125 if !confirm(" Test failed, retry? [Y/n]: ", true) {
126 println!(" {}", Color("Marked as failed.", "255;107;107"));
127 break;
128 }
129 println!(" {}", Color("Retrying step...", "255;214;102"));
130 }
131}
132
133fn test_message_box() {
134 println!("\n{}", Color("==== Testing MessageBox ====", "120;190;255"));
135
136 let icons: &[rustydialogs::MessageIcon] = &[
137 rustydialogs::MessageIcon::Info,
138 rustydialogs::MessageIcon::Warning,
139 rustydialogs::MessageIcon::Error,
140 rustydialogs::MessageIcon::Question,
141 ];
142
143 let matrix: &[(rustydialogs::MessageButtons, &[Option<rustydialogs::MessageResult>])] = &[
144 (rustydialogs::MessageButtons::Ok, &[Some(rustydialogs::MessageResult::Ok), None]),
145 (rustydialogs::MessageButtons::OkCancel, &[Some(rustydialogs::MessageResult::Ok), Some(rustydialogs::MessageResult::Cancel), None]),
146 (rustydialogs::MessageButtons::YesNo, &[Some(rustydialogs::MessageResult::Yes), Some(rustydialogs::MessageResult::No), None]),
147 (rustydialogs::MessageButtons::YesNoCancel, &[Some(rustydialogs::MessageResult::Yes), Some(rustydialogs::MessageResult::No), Some(rustydialogs::MessageResult::Cancel), None]),
148 ];
149
150 for &icon in icons {
151 println!("\n{} Icon: {}", Color("Testing", "120;190;255"), Color(format_args!("{:?}", icon), "255;214;102"));
152 let title = format!("[tests] MessageBox - {icon:?}");
153 for &(buttons, results) in matrix {
154 for &result in results {
155 let desc = match result {
156 Some(rustydialogs::MessageResult::Ok) => "Press OK.",
157 Some(rustydialogs::MessageResult::Cancel) => "Press Cancel.",
158 Some(rustydialogs::MessageResult::Yes) => "Press Yes.",
159 Some(rustydialogs::MessageResult::No) => "Press No.",
160 None => "Dismiss the dialog.",
161 };
162 let message = format!("Instruction: {desc}");
163 let full_desc = format!("{desc}\n Buttons: {}\n Icon: {}", Color(format_args!("{:?}", buttons), "255;214;102"), Color(format_args!("{:?}", icon), "255;214;102"));
164 step(&full_desc,
165 result,
166 || rustydialogs::MessageBox {
167 title: &title,
168 message: &message,
169 icon,
170 buttons,
171 owner: None,
172 }.show()
173 );
174 }
175 }
176 }
177}
178
179fn test_save_file_dialog() {
180 println!("\n{}", Color("==== Testing SaveFileDialog ====", "120;190;255"));
181
182 step("Select `readme.md` and press Save.",
183 Some(env::current_dir().unwrap().join("readme.md")),
184 || rustydialogs::FileDialog {
185 title: "[tests] SaveFileDialog",
186 path: None,
187 filter: Some(&[
188 rustydialogs::FileFilter {
189 desc: "Markdown Files",
190 patterns: &["*.md"],
191 },
192 rustydialogs::FileFilter {
193 desc: "Text Files",
194 patterns: &["*.txt"],
195 },
196 ]),
197 owner: None,
198 }.save_file()
199 );
200
201 step("Dismiss the dialog.",
202 None,
203 || rustydialogs::FileDialog {
204 title: "[tests] Dismiss SaveFileDialog",
205 path: None,
206 filter: Some(&[
207 rustydialogs::FileFilter {
208 desc: "Text Files",
209 patterns: &["*.txt"],
210 },
211 ]),
212 owner: None,
213 }.save_file()
214 );
215}
216
217fn test_open_file_dialog() {
218 println!("\n{}", Color("==== Testing OpenFileDialog ====", "120;190;255"));
219
220 step("Select `Cargo.toml` and press Open.",
221 Some(env::current_dir().unwrap().join("Cargo.toml")),
222 || rustydialogs::FileDialog {
223 title: "[tests] OpenFileDialog",
224 path: None,
225 filter: Some(&[
226 rustydialogs::FileFilter {
227 desc: "TOML Files",
228 patterns: &["*.toml"],
229 },
230 ]),
231 owner: None,
232 }.pick_file()
233 );
234 step("Select multiple files (`Cargo.toml` and `readme.md`) and press Open.",
235 Some(vec![
236 env::current_dir().unwrap().join("Cargo.toml"),
237 env::current_dir().unwrap().join("readme.md"),
238 ]),
239 || rustydialogs::FileDialog {
240 title: "[tests] OpenFileDialog (multiple)",
241 path: None,
242 filter: None,
243 owner: None,
244 }.pick_files()
245 );
246 step("Dismiss the dialog.",
247 None,
248 || rustydialogs::FileDialog {
249 title: "[tests] Dismiss OpenFileDialog",
250 path: None,
251 filter: Some(&[
252 rustydialogs::FileFilter {
253 desc: "TOML Files",
254 patterns: &["*.toml"],
255 },
256 ]),
257 owner: None,
258 }.pick_file()
259 );
260}
261
262fn test_folder_dialog() {
263 println!("\n{}", Color("==== Testing FolderDialog ====", "120;190;255"));
264
265 step("Select the `src` folder and press Open.",
266 Some(env::current_dir().unwrap().join("src")),
267 || rustydialogs::FolderDialog {
268 title: "[tests] FolderDialog",
269 directory: None,
270 owner: None,
271 }.show()
272 );
273
274 step("Dismiss the dialog.",
275 None,
276 || rustydialogs::FolderDialog {
277 title: "[tests] Dismiss FolderDialog",
278 directory: None,
279 owner: None,
280 }.show()
281 );
282}
283
284fn test_color_picker() {
285 println!("\n{}", Color("==== Testing ColorPicker ====", "120;190;255"));
286
287 step("Select pure RED (#FF0000) and press OK.",
288 Some(rustydialogs::ColorValue { red: 255, green: 0, blue: 0 }),
289 || rustydialogs::ColorPicker {
290 title: "[tests] ColorPicker",
291 value: rustydialogs::ColorValue { red: 255, green: 0, blue: 0 },
292 owner: None,
293 }.show()
294 );
295
296 step("Select specific color (#4FB3A3) (79, 179, 163) and press OK.",
297 Some(rustydialogs::ColorValue { red: 79, green: 179, blue: 163 }),
298 || rustydialogs::ColorPicker {
299 title: "[tests] ColorPicker",
300 value: rustydialogs::ColorValue { red: 255, green: 0, blue: 0 },
301 owner: None,
302 }.show()
303 );
304
305 step("Dismiss the dialog.",
306 None,
307 || rustydialogs::ColorPicker {
308 title: "[tests] Dismiss ColorPicker",
309 value: rustydialogs::ColorValue { red: 255, green: 0, blue: 0 },
310 owner: None,
311 }.show()
312 );
313}
314
315fn test_text_input() {
316 println!("\n{}", Color("==== Testing TextInput ====", "120;190;255"));
317
318 step("Enter `Hello, Rust!` and press OK.",
319 Some("Hello, Rust!".to_string()),
320 || rustydialogs::TextInput {
321 title: "[tests] TextInput",
322 message: "Instruction: Enter `Hello, Rust!` and press OK.",
323 value: "",
324 mode: rustydialogs::TextInputMode::SingleLine,
325 owner: None,
326 }.show()
327 );
328
329 step("Enter `Password123` and press OK.",
330 Some(String::from("Password123")),
331 || rustydialogs::TextInput {
332 title: "[tests] TextInput",
333 message: "Instruction: Enter `Password123` and press OK.",
334 value: "",
335 mode: rustydialogs::TextInputMode::Password,
336 owner: None,
337 }.show()
338 );
339
340 step("Enter these three lines and press OK.",
341 Some(String::from("Line 1\nLine 2\nLine 3")),
342 || rustydialogs::TextInput {
343 title: "[tests] TextInput",
344 message: "Instruction: Enter these three lines and press OK.\nLine 1\nLine 2\nLine 3",
345 value: "",
346 mode: rustydialogs::TextInputMode::MultiLine,
347 owner: None,
348 }.show()
349 );
350
351 step("Dismiss the dialog.",
352 None,
353 || rustydialogs::TextInput {
354 title: "[tests] Dismiss TextInput",
355 message: "Instruction: Dismiss the dialog (e.g. by pressing Esc or clicking the close button).",
356 value: "",
357 mode: rustydialogs::TextInputMode::SingleLine,
358 owner: None,
359 }.show()
360 );
361}
362
363fn test_notification() {
364 println!("\n{}", Color("==== Testing Notification ====", "120;190;255"));
365
366 fn notify(p: &rustydialogs::Notification<'_>) {
367 println!("\n{} Confirm {} appeared.", Color("Step:", "255;214;102"), Color(format_args!("{:?}", p.icon), "255;214;102"));
368 loop {
369 p.show();
370 if confirm("Confirm notification? [Y/n]: ", true) {
371 println!(" Result: {}", Color("PASS", "123;201;111"));
372 break;
373 }
374 println!(" Result: {}", Color("FAIL", "255;107;107"));
375 if !confirm(" Test failed, retry? [Y/n]: ", true) {
376 break;
377 }
378 }
379 }
380
381 notify(&rustydialogs::Notification {
382 app_id: "rustydialogs-tests",
383 title: "[INFO] Notification",
384 message: "This is a test notification.\nIt should appear as a native notification on your system.",
385 icon: rustydialogs::MessageIcon::Info,
386 timeout: rustydialogs::Notification::SHORT_TIMEOUT,
387 });
388
389 notify(&rustydialogs::Notification {
390 app_id: "rustydialogs-tests",
391 title: "[WARN] Notification",
392 message: "This is a test notification.\nIt should appear as a native notification on your system.",
393 icon: rustydialogs::MessageIcon::Warning,
394 timeout: rustydialogs::Notification::SHORT_TIMEOUT,
395 });
396
397 notify(&rustydialogs::Notification {
398 app_id: "rustydialogs-tests",
399 title: "[ERROR] Notification",
400 message: "This is a test notification.\nIt should appear as a native notification on your system.",
401 icon: rustydialogs::MessageIcon::Error,
402 timeout: rustydialogs::Notification::SHORT_TIMEOUT,
403 });
404
405 notify(&rustydialogs::Notification {
406 app_id: "rustydialogs-tests",
407 title: "[QUESTION] Notification",
408 message: "This is a test notification.\nIt should appear as a native notification on your system.",
409 icon: rustydialogs::MessageIcon::Question,
410 timeout: rustydialogs::Notification::SHORT_TIMEOUT,
411 });
412}
413
414fn confirm(prompt: &str, default: bool) -> bool {
415 loop {
416 let input = prompt_input(prompt);
417 let value = input.trim().to_ascii_lowercase();
418 if value.is_empty() {
419 return default;
420 }
421 if value == "y" || value == "yes" {
422 return true;
423 }
424 if value == "n" || value == "no" {
425 return false;
426 }
427 eprintln!("{}", Color("Please answer y/yes or n/no.", "255;107;107"));
428 }
429}
430
431struct Color<'a, T> {
432 value: T,
433 color: &'a str,
434}
435#[allow(non_snake_case)]
436fn Color<'a, T>(value: T, color: &'a str) -> Color<'a, T> {
437 Color { value, color }
438}
439impl<'a, T: fmt::Display> fmt::Display for Color<'a, T> {
440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 if !io::stdout().is_terminal() {
442 write!(f, "{}", self.value)
443 }
444 else {
445 write!(f, "\x1b[38;2;{}m{}\x1b[0m", self.color, self.value)
446 }
447 }
448}