tfd/
lib.rs

1//! # tinyfiledialogs-rs
2//!
3//! A pure Rust implementation of the tinyfiledialogs library.
4//! Based on the original C library by Guillaume Vareille.
5//!
6//! ## Security Warning
7//!
8//! tinyfiledialogs should only be used with trusted input. Using it with
9//! untrusted input, for example as dialog title or message, can in the worst
10//! case lead to execution of arbitrary commands.
11
12use std::path::{Path, PathBuf};
13
14// Platform-specific modules
15#[cfg(target_os = "macos")]
16mod macos;
17#[cfg(all(unix, not(target_os = "macos")))]
18mod unix;
19#[cfg(target_os = "windows")]
20mod windows;
21#[cfg(target_os = "ios")]
22mod ios;
23#[cfg(target_os = "android")]
24mod android;
25
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum MessageBoxIcon {
28    Info,
29    Warning,
30    Error,
31    Question,
32}
33
34impl MessageBoxIcon {
35    fn to_str(&self) -> &'static str {
36        match *self {
37            MessageBoxIcon::Info => "info",
38            MessageBoxIcon::Warning => "warning",
39            MessageBoxIcon::Error => "error",
40            MessageBoxIcon::Question => "question",
41        }
42    }
43}
44
45#[derive(Debug, PartialEq, Copy, Clone)]
46pub enum OkCancel {
47    Cancel = 0,
48    Ok = 1,
49}
50
51#[derive(Debug, PartialEq, Copy, Clone)]
52pub enum YesNo {
53    No = 0,
54    Yes = 1,
55}
56
57#[derive(Debug, PartialEq, Copy, Clone)]
58pub enum YesNoCancel {
59    Cancel = 0,
60    Yes = 1,
61    No = 2,
62}
63
64// Base dialog struct
65pub struct Dialog {
66    title: String,
67    message: String,
68}
69
70impl Dialog {
71    pub fn new<S: Into<String>, Q: Into<String>>(title: S, message: Q) -> Self {
72        Self {
73            title: title.into(),
74            message: message.into(),
75        }
76    }
77
78    pub fn title(&self) -> &str {
79        &self.title
80    }
81
82    pub fn message(&self) -> &str {
83        &self.message
84    }
85
86    pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
87        self.title = title.into();
88        self
89    }
90
91    pub fn with_message<S: Into<String>>(mut self, message: S) -> Self {
92        self.message = message.into();
93        self
94    }
95
96    /// Sanitize input for shell execution
97    fn sanitize_input(input: &str) -> String {
98        input
99            .replace("\"", "\\\"")
100            .replace("'", "\\'")
101            .replace("`", "\\`")
102    }
103
104    /// Verify path exists
105    fn verify_path(path: &str) -> Option<PathBuf> {
106        let path = Path::new(path);
107        if path.exists() {
108            Some(path.to_path_buf())
109        } else {
110            None
111        }
112    }
113}
114
115// Message Box
116pub struct MessageBox {
117    dialog: Dialog,
118    icon: MessageBoxIcon,
119}
120
121impl MessageBox {
122    pub fn new<S: Into<String>>(title: S, message: S) -> Self {
123        Self {
124            dialog: Dialog::new(title, message),
125            icon: MessageBoxIcon::Info,
126        }
127    }
128
129    pub fn with_icon(mut self, icon: MessageBoxIcon) -> Self {
130        self.icon = icon;
131        self
132    }
133
134    pub fn icon(&self) -> MessageBoxIcon {
135        self.icon
136    }
137
138    pub fn run_modal(&self) {
139        #[cfg(target_os = "macos")]
140        macos::message_box_ok(self);
141
142        #[cfg(all(unix, not(target_os = "macos")))]
143        unix::message_box_ok(self);
144
145        #[cfg(target_os = "windows")]
146        windows::message_box_ok(self);
147    }
148
149    pub fn run_modal_ok_cancel(&self, default: OkCancel) -> OkCancel {
150        #[cfg(target_os = "macos")]
151        return macos::message_box_ok_cancel(self, default);
152
153        #[cfg(all(unix, not(target_os = "macos")))]
154        return unix::message_box_ok_cancel(self, default);
155
156        #[cfg(target_os = "windows")]
157        return windows::message_box_ok_cancel(self, default);
158
159        #[allow(unreachable_code)]
160        OkCancel::Cancel
161    }
162
163    pub fn run_modal_yes_no(&self, default: YesNo) -> YesNo {
164        #[cfg(target_os = "macos")]
165        return macos::message_box_yes_no(self, default);
166
167        #[cfg(all(unix, not(target_os = "macos")))]
168        return unix::message_box_yes_no(self, default);
169
170        #[cfg(target_os = "windows")]
171        return windows::message_box_yes_no(self, default);
172
173        #[allow(unreachable_code)]
174        YesNo::No
175    }
176
177    pub fn run_modal_yes_no_cancel(&self, default: YesNoCancel) -> YesNoCancel {
178        #[cfg(target_os = "macos")]
179        return macos::message_box_yes_no_cancel(self, default);
180
181        #[cfg(all(unix, not(target_os = "macos")))]
182        return unix::message_box_yes_no_cancel(self, default);
183
184        #[cfg(target_os = "windows")]
185        return windows::message_box_yes_no_cancel(self, default);
186
187        #[allow(unreachable_code)]
188        YesNoCancel::Cancel
189    }
190}
191
192// Input Box
193pub struct InputBox {
194    dialog: Dialog,
195    default_value: Option<String>,
196    is_password: bool,
197}
198
199impl InputBox {
200    pub fn new<S: Into<String>>(title: S, message: S) -> Self {
201        Self {
202            dialog: Dialog::new(title, message),
203            default_value: None,
204            is_password: false,
205        }
206    }
207
208    pub fn with_default<S: Into<String>>(mut self, default: S) -> Self {
209        self.default_value = Some(default.into());
210        self
211    }
212
213    pub fn password(mut self, is_password: bool) -> Self {
214        self.is_password = is_password;
215        self
216    }
217
218    pub fn default_value(&self) -> Option<&str> {
219        self.default_value.as_deref()
220    }
221
222    pub fn is_password(&self) -> bool {
223        self.is_password
224    }
225
226    pub fn run_modal(&self) -> Option<String> {
227        #[cfg(target_os = "macos")]
228        return macos::input_box(self);
229
230        #[cfg(all(unix, not(target_os = "macos")))]
231        return unix::input_box(self);
232
233        #[cfg(target_os = "windows")]
234        return windows::input_box(self);
235
236        #[allow(unreachable_code)]
237        None
238    }
239}
240
241// File Dialog
242pub struct FileDialog {
243    dialog: Dialog,
244    path: String,
245    filter_patterns: Vec<String>,
246    filter_description: String,
247    multiple_selection: bool,
248}
249
250impl FileDialog {
251    pub fn new<S: Into<String>>(title: S) -> Self {
252        Self {
253            dialog: Dialog::new(title, ""),
254            path: String::new(),
255            filter_patterns: Vec::new(),
256            filter_description: String::new(),
257            multiple_selection: false,
258        }
259    }
260
261    pub fn with_path<S: Into<String>>(mut self, path: S) -> Self {
262        self.path = path.into();
263        self
264    }
265
266    pub fn with_filter<S: Into<String>>(mut self, patterns: &[&str], description: S) -> Self {
267        self.filter_patterns = patterns.iter().map(|&s| s.to_string()).collect();
268        self.filter_description = description.into();
269        self
270    }
271
272    pub fn with_multiple_selection(mut self, allow_multi: bool) -> Self {
273        self.multiple_selection = allow_multi;
274        self
275    }
276
277    pub fn path(&self) -> &str {
278        &self.path
279    }
280
281    pub fn filter_patterns(&self) -> &[String] {
282        &self.filter_patterns
283    }
284
285    pub fn filter_description(&self) -> &str {
286        &self.filter_description
287    }
288
289    pub fn multiple_selection(&self) -> bool {
290        self.multiple_selection
291    }
292
293    pub fn save_file(&self) -> Option<String> {
294        #[cfg(target_os = "macos")]
295        return macos::save_file_dialog(self);
296
297        #[cfg(all(unix, not(target_os = "macos")))]
298        return unix::save_file_dialog(self);
299
300        #[cfg(target_os = "windows")]
301        return windows::save_file_dialog(self);
302
303        #[allow(unreachable_code)]
304        None
305    }
306
307    pub fn open_file(&self) -> Option<String> {
308        self.open_files().and_then(|v| v.into_iter().next())
309    }
310
311    pub fn open_files(&self) -> Option<Vec<String>> {
312        #[cfg(target_os = "macos")]
313        return macos::open_file_dialog(self);
314
315        #[cfg(all(unix, not(target_os = "macos")))]
316        return unix::open_file_dialog(self);
317
318        #[cfg(target_os = "windows")]
319        return windows::open_file_dialog(self);
320
321        #[allow(unreachable_code)]
322        None
323    }
324
325    pub fn select_folder(&self) -> Option<String> {
326        #[cfg(target_os = "macos")]
327        return macos::select_folder_dialog(self);
328
329        #[cfg(all(unix, not(target_os = "macos")))]
330        return unix::select_folder_dialog(self);
331
332        #[cfg(target_os = "windows")]
333        return windows::select_folder_dialog(self);
334
335        #[allow(unreachable_code)]
336        None
337    }
338}
339
340pub enum DefaultColorValue {
341    Hex(String),
342    RGB([u8; 3]),
343}
344
345pub struct ColorChooser {
346    dialog: Dialog,
347    default_color: DefaultColorValue,
348}
349
350impl ColorChooser {
351    pub fn new<S: Into<String>>(title: S) -> Self {
352        Self {
353            dialog: Dialog::new(title, String::new()),
354            default_color: DefaultColorValue::RGB([0, 0, 0]),
355        }
356    }
357
358    pub fn with_default_color(mut self, default: DefaultColorValue) -> Self {
359        self.default_color = default;
360        self
361    }
362
363    pub fn default_color(&self) -> &DefaultColorValue {
364        &self.default_color
365    }
366
367    pub fn run_modal(&self) -> Option<(String, [u8; 3])> {
368        #[cfg(target_os = "macos")]
369        return macos::color_chooser_dialog(self);
370
371        #[cfg(all(unix, not(target_os = "macos")))]
372        return unix::color_chooser_dialog(self);
373
374        #[cfg(target_os = "windows")]
375        return windows::color_chooser_dialog(self);
376
377        #[allow(unreachable_code)]
378        None
379    }
380}
381
382pub struct Notification {
383    title: String,
384    message: String,
385    subtitle: Option<String>,
386    sound: Option<String>,
387}
388
389impl Notification {
390    pub fn new<S: Into<String>>(title: S, message: S) -> Self {
391        Self {
392            title: title.into(),
393            message: message.into(),
394            subtitle: None,
395            sound: None,
396        }
397    }
398
399    pub fn with_subtitle<S: Into<String>>(mut self, subtitle: S) -> Self {
400        self.subtitle = Some(subtitle.into());
401        self
402    }
403
404    pub fn with_sound<S: Into<String>>(mut self, sound: S) -> Self {
405        self.sound = Some(sound.into());
406        self
407    }
408
409    pub fn title(&self) -> &str {
410        &self.title
411    }
412
413    pub fn message(&self) -> &str {
414        &self.message
415    }
416
417    pub fn subtitle(&self) -> Option<&str> {
418        self.subtitle.as_deref()
419    }
420
421    pub fn sound(&self) -> Option<&str> {
422        self.sound.as_deref()
423    }
424
425    pub fn show(&self) -> bool {
426        #[cfg(target_os = "macos")]
427        return macos::notification(self);
428
429        #[cfg(all(unix, not(target_os = "macos")))]
430        return unix::notification(self);
431
432        #[cfg(target_os = "windows")]
433        return windows::notification(self);
434
435        #[allow(unreachable_code)]
436        false
437    }
438}
439
440// Utility functions
441fn hex_to_rgb(hex: &str) -> [u8; 3] {
442    let hex = hex.trim_start_matches('#');
443    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
444    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
445    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
446    [r, g, b]
447}
448
449fn rgb_to_hex(rgb: &[u8; 3]) -> String {
450    format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])
451}