Skip to main content

fude/
dialogs.rs

1//! Native dialogs backed by rfd. Registered by [`crate::App::with_dialogs`].
2//!
3//! On macOS, `NSOpenPanel` / `NSSavePanel` / `NSAlert` must run on the
4//! main (UI) thread. Because fude's IPC handler runs on a background
5//! thread, every dialog call is dispatched via [`crate::MainDispatcher`]
6//! and blocks the caller until the user dismisses the dialog.
7
8use rfd::{FileDialog, MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};
9use serde_json::Value;
10
11use crate::MainDispatcher;
12
13fn arg_str<'a>(args: &'a Value, key: &str) -> Option<&'a str> {
14    args.get(key).and_then(|v| v.as_str())
15}
16
17fn with_filters(mut dialog: FileDialog, args: &Value) -> FileDialog {
18    if let Some(filters) = args.get("filters").and_then(|v| v.as_array()) {
19        for f in filters {
20            let name = f.get("name").and_then(|v| v.as_str()).unwrap_or("");
21            let exts: Vec<&str> = f
22                .get("extensions")
23                .and_then(|v| v.as_array())
24                .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
25                .unwrap_or_default();
26            if !exts.is_empty() {
27                dialog = dialog.add_filter(name, &exts);
28            }
29        }
30    }
31    if let Some(dir) = arg_str(args, "defaultPath") {
32        dialog = dialog.set_directory(dir);
33    }
34    dialog
35}
36
37pub(crate) fn open(main: &MainDispatcher, args: &Value) -> Result<Value, String> {
38    let args = args.clone();
39    main.run(move || {
40        let directory = args
41            .get("directory")
42            .and_then(|v| v.as_bool())
43            .unwrap_or(false);
44        let multiple = args
45            .get("multiple")
46            .and_then(|v| v.as_bool())
47            .unwrap_or(false);
48        let dialog = with_filters(FileDialog::new(), &args);
49
50        if directory {
51            if multiple {
52                dialog
53                    .pick_folders()
54                    .map(|v| {
55                        Value::Array(
56                            v.into_iter()
57                                .map(|p| Value::from(p.to_string_lossy().to_string()))
58                                .collect(),
59                        )
60                    })
61                    .unwrap_or(Value::Null)
62            } else {
63                dialog
64                    .pick_folder()
65                    .map(|p| Value::from(p.to_string_lossy().to_string()))
66                    .unwrap_or(Value::Null)
67            }
68        } else if multiple {
69            dialog
70                .pick_files()
71                .map(|v| {
72                    Value::Array(
73                        v.into_iter()
74                            .map(|p| Value::from(p.to_string_lossy().to_string()))
75                            .collect(),
76                    )
77                })
78                .unwrap_or(Value::Null)
79        } else {
80            dialog
81                .pick_file()
82                .map(|p| Value::from(p.to_string_lossy().to_string()))
83                .unwrap_or(Value::Null)
84        }
85    })
86}
87
88pub(crate) fn save(main: &MainDispatcher, args: &Value) -> Result<Value, String> {
89    let args = args.clone();
90    main.run(move || {
91        with_filters(FileDialog::new(), &args)
92            .save_file()
93            .map(|p| Value::from(p.to_string_lossy().to_string()))
94            .unwrap_or(Value::Null)
95    })
96}
97
98pub(crate) fn ask(main: &MainDispatcher, args: &Value) -> Result<Value, String> {
99    let args = args.clone();
100    main.run(move || {
101        let message = arg_str(&args, "message").unwrap_or("");
102        let title = arg_str(&args, "title").unwrap_or("");
103        let ok_label = arg_str(&args, "okLabel");
104        let cancel_label = arg_str(&args, "cancelLabel");
105        let buttons = match (ok_label, cancel_label) {
106            (Some(ok), Some(cancel)) => {
107                MessageButtons::OkCancelCustom(ok.to_string(), cancel.to_string())
108            }
109            _ => MessageButtons::YesNo,
110        };
111        let result = MessageDialog::new()
112            .set_title(title)
113            .set_description(message)
114            .set_level(MessageLevel::Info)
115            .set_buttons(buttons)
116            .show();
117        let ok = matches!(
118            result,
119            MessageDialogResult::Yes | MessageDialogResult::Ok | MessageDialogResult::Custom(_)
120        );
121        Value::from(ok)
122    })
123}
124
125pub(crate) fn message(main: &MainDispatcher, args: &Value) -> Result<Value, String> {
126    let args = args.clone();
127    main.run(move || {
128        let body = arg_str(&args, "message").unwrap_or("");
129        let title = arg_str(&args, "title").unwrap_or("");
130        let kind = arg_str(&args, "kind").unwrap_or("info");
131        let level = match kind {
132            "error" => MessageLevel::Error,
133            "warning" => MessageLevel::Warning,
134            _ => MessageLevel::Info,
135        };
136        MessageDialog::new()
137            .set_title(title)
138            .set_description(body)
139            .set_level(level)
140            .set_buttons(MessageButtons::Ok)
141            .show();
142        Value::Null
143    })
144}